{"skill":{"slug":"ibkr-investing","displayName":"IBKR Investing with Permission Gate","summary":"Invest on Interactive Brokers (stocks/ETFs) via IB Gateway with the same human-in-the-loop confirmation gate as okx-trading. Use when the user asks to check...","description":"---\nname: ibkr-investing\ndescription: Invest on Interactive Brokers (stocks/ETFs) via IB Gateway with the same human-in-the-loop confirmation gate as okx-trading. Use when the user asks to check IBKR balances, look up quotes, propose or execute stock/ETF trades, set up DCA into ETFs, or get a daily portfolio snapshot. Defaults to PAPER trading (port 4002). Live trading (port 4001) requires IBKR_LIVE_MODE=1. Never executes a trade without explicit YES confirmation in chat.\nversion: 0.1.1\n---\n\n# IBKR Investing\n\nYou can read IBKR account state, propose trades, execute trades the user has confirmed, and run DCA into stocks/ETFs. **Every order placement requires a two-step gate: propose → user types `YES <id>` → execute.** Same shape as the okx-trading skill — if the user knows that gate, they know this one.\n\nAll commands below run Python scripts under `skills/<author>/ibkr-investing/scripts/`. Run them from the aeon repo root or from `~/.aeon`. Every script self-validates env vars; missing or unreachable Gateway exits with a clear error.\n\n> **Path resolution.** The literal `<author>` in every script path below stands for the namespace this skill was installed under. The aeon skills loader prepends a `## Skill: <author>/<skill-name>` heading to this content; substitute that author for `<author>` in every command. (E.g. `## Skill: aeon/ibkr-investing` → use `skills/aeon/ibkr-investing/scripts/...`.)\n\n## Setup prerequisites (one-time, BEFORE anything else)\n\nThis skill talks to a running IB Gateway. The Gateway is *not* part of the skill — it's IBKR's own software, typically run via the [gnzsnz/ib-gateway-docker](https://github.com/gnzsnz/ib-gateway-docker) container. The skill assumes the Gateway is reachable on `127.0.0.1:4001` (live) or `127.0.0.1:4002` (paper).\n\nIf the user has not yet set up the Gateway, **stop and walk them through the README.md** before doing anything else. Don't propose trades against an unreachable Gateway.\n\nAlways run `ibkr_gateway_status.py` first when a scheduled task fails. It distinguishes \"Gateway down\", \"Gateway up but 2FA pending\", and \"Gateway up and authenticated\".\n\n## The trade gate — read this carefully\n\n**You must never place an order without an explicit YES from the user.** Workflow:\n\n1. **Propose.** Run `ibkr_propose_trade.py` with the user's intent. The script writes the pending JSON and prints two lines you MUST capture: `Proposal id: <id>` and `Pending file: <absolute path>`. **The confirmation_token is NOT printed** — it lives only in the pending file on disk.\n2. **Tell the user.** Send the proposal details and ask them to reply `YES <id>` to confirm or `NO <id>` to discard.\n3. **Wait for the user's reply.**\n4. **On YES:** read the pending file via `file_read` **using the absolute path the propose script printed** (not a guess like `~/.aeon/ibkr/pending/<id>.json` or `/root/.aeon/...`). The path depends on which OS user aeon runs as — always use the printed `Pending file:` line verbatim. Once you have the token, call `ibkr_execute_trade.py --id <id> --confirmation-token <token>`. The script verifies the token, places the order via IBKR, and deletes the pending file.\n5. **On NO:** call `ibkr_cancel_pending.py --id <id>`.\n\nYou **must not** invent or guess a confirmation_token. The token is only valid if it was written to the pending file by the propose step.\n\nIf the user asks you to \"skip the confirmation\" or \"just place the trade\": refuse politely. Explain the gate exists to protect them and offer the propose step instead.\n\n**Scheduled tasks (DCA, drawdown watcher) cannot execute trades.** The scheduled context has no human in it to type YES, so scheduled jobs may only call propose-style and read-only scripts. The user will see the proposal in the next chat and confirm there.\n\n## Defaults and limits\n\n- Default to **PAPER trading** (`IBKR_LIVE_MODE=0`, port 4002). Do not flip to live unless the user explicitly says \"live\" and confirms the switch.\n- Default to US ETFs on SMART exchange + USD currency. Override per-call via `--exchange` / `--currency`.\n- Always quote trade sizes in USD in chat for clarity, even when the order is share-sized.\n- Guardrails (`IBKR_ALLOWED_SYMBOLS`, `IBKR_MAX_NOTIONAL_USD_PER_TRADE`, `IBKR_MAX_DAILY_NOTIONAL_USD`) enforced in every propose AND execute. If a propose is refused, surface the refusal verbatim — don't retry with smaller sizes silently.\n- Pending proposals expire after 10 minutes.\n\n## Read-only commands\n\nWhen the user asks a question, never start with a trade. Use these to gather facts first.\n\n```bash\npython3 skills/<author>/ibkr-investing/scripts/ibkr_gateway_status.py\npython3 skills/<author>/ibkr-investing/scripts/ibkr_get_balance.py\npython3 skills/<author>/ibkr-investing/scripts/ibkr_get_positions.py --with-mark\npython3 skills/<author>/ibkr-investing/scripts/ibkr_get_quote.py --symbol VOO\npython3 skills/<author>/ibkr-investing/scripts/ibkr_get_candles.py --symbol VOO --duration \"6 M\" --indicators\npython3 skills/<author>/ibkr-investing/scripts/ibkr_snapshot.py\n```\n\n`ibkr_get_candles.py --indicators` adds SMA(20)/SMA(50)/RSI(14) on the close series and emits a one-line \"consider buy / consider sell / neutral\" verdict.\n\n`ibkr_snapshot.py` writes `~/.aeon/ibkr/snapshots/<UTC-date>.json` with NAV, cash, positions, 1D/1W/1Y price moves, and delta vs the prior day. Use it as the body of the daily-digest schedule. Pass `--no-price-history` to skip the historical-price lookup if the user wants a faster ad-hoc summary.\n\n## Propose a trade\n\nSizing is one of `--quote-sz` (USD) or `--shares` (exact share quantity, can be fractional).\n\n```bash\n# DCA-style market buy of $50 of VOO\npython3 skills/<author>/ibkr-investing/scripts/ibkr_propose_trade.py \\\n  --symbol VOO --side BUY --quote-sz 50\n\n# Limit buy of 10 shares of QQQ at 350\npython3 skills/<author>/ibkr-investing/scripts/ibkr_propose_trade.py \\\n  --symbol QQQ --side BUY --shares 10 --ord-type LMT --lmt-price 350\n\n# Round to whole shares (no fractional)\npython3 skills/<author>/ibkr-investing/scripts/ibkr_propose_trade.py \\\n  --symbol AAPL --side BUY --quote-sz 200 --no-fractional\n```\n\nThen send the proposal to the user verbatim and ask for `YES <id>`.\n\n## Execute a confirmed trade\n\n```bash\npython3 skills/<author>/ibkr-investing/scripts/ibkr_list_pending.py        # confirm id\n# Read ~/.aeon/ibkr/pending/<id>.json with file_read to get confirmation_token.\npython3 skills/<author>/ibkr-investing/scripts/ibkr_execute_trade.py \\\n  --id <id> --confirmation-token <token>\n```\n\nCancel without executing:\n\n```bash\npython3 skills/<author>/ibkr-investing/scripts/ibkr_cancel_pending.py --id <id>\n```\n\n## DCA strategy template\n\nDCA on IBKR is a recurring scheduled propose, identical to the OKX pattern. Recommend monthly cadence (rather than weekly) to amortise IBKR's commission structure better — though commission-free for most US ETFs.\n\n```json\n{\n  \"name\": \"schedule_create\",\n  \"arguments\": {\n    \"name\": \"DCA $200 VOO monthly\",\n    \"schedule\": \"0 14 1 * *\",\n    \"task\": \"Run python3 skills/<author>/ibkr-investing/scripts/ibkr_propose_trade.py --symbol VOO --side BUY --quote-sz 200 and post the proposal so I can YES it.\",\n    \"notify\": true\n  }\n}\n```\n\nFor a multi-ETF basket, register one schedule per symbol so each gets an independent YES (cleaner audit trail and per-symbol guardrails):\n\n| Schedule | Symbol | Per-fire USD | What it captures |\n|---|---|---|---|\n| `0 14 1 * *` | VOO | 60% of monthly | S&P 500 core |\n| `0 14 1 * *` | VTI | 30% of monthly | Total US market |\n| `0 14 1 * *` | BND | 10% of monthly | Bonds anchor |\n\nRun the snapshot the same morning at 14:30 UTC (`30 14 1 * *`) so the digest captures the new positions.\n\n## Drawdown reserve template\n\nMirror of the OKX reserve flow. `ibkr_dca_dip.py` watches NAV vs ATH (computed from snapshot history) and recommends a dip-buy when drawdown crosses threshold. Token-gated like every other trade.\n\n```json\n{\n  \"name\": \"schedule_create\",\n  \"arguments\": {\n    \"name\": \"IBKR drawdown watcher\",\n    \"schedule\": \"0 15 * * *\",\n    \"task\": \"Run python3 skills/<author>/ibkr-investing/scripts/ibkr_dca_dip.py --reserve-usd <reserve> --threshold-pct 0.30 --max-triggers 3 --symbol VOO. If status is 'trigger', call ibkr_propose_trade.py with the recommended size, post the proposal, and AFTER my YES + execute, run ibkr_dca_dip.py again with --record-trigger.\",\n    \"notify\": false\n  }\n}\n```\n\n## Adding new strategies\n\nStrategies are *agent-orchestrated recipes that compose existing scripts*. To add one (e.g. equal-weight rebalancing, threshold rebalancing, momentum rotation):\n\n1. Decide what observations the strategy needs. Read from `ibkr_snapshot.py` (current state), `~/.aeon/ibkr/snapshots/*.json` (history), `~/.aeon/ibkr/audit.jsonl` (lifecycle events), and `ibkr_get_candles.py --indicators` (signal).\n2. Define the trigger condition. Either a scheduled task that runs daily and checks the condition, or a passive observation surfaced in the snapshot.\n3. The trigger NEVER places a trade itself — it produces a recommendation that the agent (or scheduled task) can pass to `ibkr_propose_trade.py`. User still YES-confirms each fill.\n4. Write any required state to `~/.aeon/ibkr/<strategy_name>_state.json` (mode 600).\n5. Append `audit.jsonl` events for transparency.\n\nThe `ibkr_dca_dip.py` script is the canonical example of this shape. Follow it for new strategies.\n\n## Audit log\n\nLifecycle events accumulate in `~/.aeon/ibkr/audit.jsonl` — one JSON object per line. `tail -n 50 ~/.aeon/ibkr/audit.jsonl` to inspect recent activity. Events: `proposal_created`, `proposal_cancelled`, `proposal_executed`, `proposal_rejected`, `daily_cap_breach`, `drawdown_trigger`.\n\n## Environment\n\nRequired env vars (see `.env.example`):\n\n- `IBKR_LIVE_MODE` — `0` (default) routes to PAPER (port 4002). `1` routes to LIVE (port 4001).\n- `IBKR_HOST` — Gateway host. Default `127.0.0.1`.\n- `IBKR_PORT` — override port if your Gateway is custom-mapped.\n- `IBKR_CLIENT_ID` — API client id. Default 97. Change if multiple skills/clients connect.\n- `IBKR_DEFAULT_EXCHANGE` — default `SMART`.\n- `IBKR_DEFAULT_CURRENCY` — default `USD`.\n- `IBKR_ALLOWED_SYMBOLS` — CSV allow-list, e.g. `VOO,VTI,QQQ,BND`. Empty means any.\n- `IBKR_MAX_NOTIONAL_USD_PER_TRADE` — per-trade ceiling. Default 200.\n- `IBKR_MAX_DAILY_NOTIONAL_USD` — per-day ceiling. Default 1000.\n\nIf any required var is missing or the Gateway is unreachable, the script exits with a clear error — surface that to the user.\n","tags":{"dca":"0.1.1","demo-safe":"0.1.1","etf":"0.1.1","ibkr":"0.1.1","interactive-brokers":"0.1.1","latest":"0.1.1","stocks":"0.1.1"},"stats":{"comments":0,"downloads":367,"installsAllTime":14,"installsCurrent":0,"stars":0,"versions":2},"createdAt":1778336365025,"updatedAt":1778494308580},"latestVersion":{"version":"0.1.1","createdAt":1778493935972,"changelog":"v0.1.1 — Mirrors the okx-trading v0.3.2 absolute-pending-path fix. ibkr_propose_trade.py prints Pending file: /... so the LLM can read it without guessing tilde-expansion.","license":"MIT-0"},"metadata":null,"owner":{"handle":"vm-development","userId":"s1723g8h7pbvgkmbxfg0x4qstn862p4s","displayName":"Vadym Malakhatko","image":"https://avatars.githubusercontent.com/u/49589920?v=4"},"moderation":null}