Skip to main content
Glama
jotorresro

mcp-ibkr

by jotorresro
README.md
# mcp-ibkr

Servidor MCP (Model Context Protocol) propio que conecta Claude Code con dos
capacidades independientes:

- **IBKR** — consultas de solo lectura sobre una cuenta de **Paper Trading**
  de Interactive Brokers.
- **Research** — un sistema de investigación que procesa, indexa y permite
  buscar dentro de documentos (papers, PDFs) que **el usuario provee
  explícitamente**. Nunca busca, descubre ni descarga documentos por su
  cuenta.

> Estado del proyecto: IBKR (12 fases) y Research (11 fases) completos. Ver
> sección "Estado actual".

## 1. Qué es este proyecto

Un puente entre Claude Code y dos fuentes de conocimiento/acción distintas:

- Claude Code lanza este servidor, el servidor expone **herramientas**
  (funciones con nombre y descripción) y **recursos** (contenido legible por
  URI), y Claude los usa cuando el usuario pide información de la cuenta de
  IBKR o quiere consultar/analizar documentos que ya ingirió.
- Ninguna herramienta de IBKR habla directamente con `ib_async`: todas pasan
  por `src/ibkr/connection.py`.
- Ningún componente de Research habla directamente con Chroma, sqlite o
  fastembed: todos pasan por las abstracciones en `src/research/`.
- **IBKR y Research son dominios completamente separados.** Ninguno importa
  código del otro. El único punto que conoce ambos es el servidor MCP
  (`src/server.py`), que simplemente registra las herramientas/recursos de
  los dos.

## 2. Arquitectura

```
Claude Code (cliente MCP)
      │  stdio / JSON-RPC
      v
Servidor MCP (src/server.py)
      │
      ├──────────────────────────┬───────────────────────────┐
      v                          v                            v
Tool Registry              Resource Registry            (ambos registros
(src/tools/*)              (src/resources/*)             son independientes
      │                          │                        entre si)
      v                          v
┌─────────────┐          ┌───────────────┐
│ IBKR tools  │          │ document://   │
│ (account,   │          │ {document_id} │
│ market,     │          └───────┬───────┘
│ positions,  │                  │
│ orders)     │                  v
└──────┬──────┘          ResearchService / IngestionService
       │                 (src/research/services/*)
       v                          │
Capa de integración IBKR          v
(src/ibkr/connection.py)  ingestion → chunking → embeddings → vector store
       │  ib_async → socket TCP   (src/research/*)
       v
IB Gateway (Paper Trading, puerto 4002)
       │
       v
Interactive Brokers
```

Research **nunca** importa nada de `src/ibkr/`, e IBKR **nunca** importa
nada de `src/research/`. Si en el futuro se agrega análisis que combine
ambos (ver sección 9.7), esa orquestación viviría en una capa nueva
separada de las dos, no dentro de ninguna de ellas.

## 3. Estructura de carpetas

```
mcp-ibkr/
├── src/
│   ├── server.py            # Punto de entrada: registra tools Y resources
│   ├── ibkr/
│   │   └── connection.py       # UNICO lugar que habla con ib_async / IB Gateway
│   ├── research/                # Dominio Research -- nunca importa src/ibkr/
│   │   ├── models.py               # Document, DocumentMetadata, Chunk, ResearchResult
│   │   ├── ingestion/
│   │   │   ├── loader.py             # Lee el archivo, detecta tipo, calcula hash
│   │   │   ├── parser.py             # Extrae texto (PdfParser via pypdf)
│   │   │   ├── normalizer.py         # Limpia artefactos de extraccion de PDF
│   │   │   └── metadata.py           # Lee /Info del PDF + DOI/arXiv ID del texto
│   │   ├── chunking/
│   │   │   └── chunker.py            # StructuralChunker: respeta parrafo/pagina/seccion
│   │   ├── embeddings/
│   │   │   └── provider.py           # EmbeddingProvider + FastEmbedProvider (fastembed)
│   │   ├── storage/
│   │   │   ├── vector_store.py       # VectorStore + ChromaVectorStore
│   │   │   └── document_store.py     # DocumentStore (sqlite): metadata + dedup por hash
│   │   ├── retrieval/
│   │   │   └── retriever.py          # Retriever + SemanticRetriever
│   │   └── services/
│   │       ├── ingestion_service.py  # Orquesta todo el pipeline de ingestion
│   │       ├── research_service.py   # Busqueda + metadata + reconstruccion de texto
│   │       └── factory.py            # UNICO lugar que cablea todo lo de arriba
│   ├── tools/
│   │   ├── registry.py       # Registro central: conecta archivos de herramienta con el servidor
│   │   ├── account/            # Saldo, resumen de cuenta, verificar conexión (IBKR)
│   │   ├── market/              # Precio, cotización, históricos (IBKR)
│   │   ├── positions/            # Posiciones abiertas, P&L (IBKR)
│   │   ├── orders/                # Consultar (activa) + crear/cancelar (ACTION, deshabilitadas) (IBKR)
│   │   └── research/               # ingest_document, list_documents, get_document, search
│   ├── resources/
│   │   ├── registry.py       # Registro central de MCP Resources (paralelo a tools/registry.py)
│   │   └── documents.py       # document://{document_id} -- contenido completo de un documento
│   ├── config/
│   │   └── settings.py       # Carga .env: load_ibkr_settings() + load_research_settings()
│   └── utils/
│       └── risk.py            # RiskLevel: READ_ONLY / ACTION (solo tiene sentido para IBKR)
├── data/
│   └── research/          # Indice vectorial + registro de documentos (gitignored, se crea solo)
├── tests/
├── .env.example
├── .gitignore
├── .mcp.json          # Registro del servidor para Claude Code (scope project)
├── pyproject.toml      # Configuracion de pytest
├── requirements.txt     # Dependencias exactas (pip freeze)
└── README.md
```

## 4. Requisitos

- Python 3.11+ (probado con 3.14).
- Git.
- `curl` (para descargar `get-pip.py` en la sección 5 y el instalador de IB Gateway en la sección 6).
- IB Gateway instalado, con sesión de **Paper Trading** iniciada (ver sección 6) — solo necesario para las herramientas de IBKR.
- Conexión a Internet **una vez**, la primera vez que se usa Research: `fastembed` descarga un modelo de embeddings pequeño (ONNX, no PyTorch) desde Hugging Face la primera vez que se genera un embedding. Después de esa primera vez, Research funciona completamente offline.

## 5. Instalación y configuración

```bash
cd ~/mcp-ibkr

# Crear entorno virtual aislado para este proyecto
python3 -m venv --without-pip .venv

# Instalar pip dentro del venv (no viene incluido con --without-pip)
curl -sS https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
.venv/bin/python3 /tmp/get-pip.py

# Instalar dependencias del proyecto
.venv/bin/python3 -m pip install -r requirements.txt

# Configuración local (nunca se sube a Git)
cp .env.example .env
```

> Nota sobre `requirements.txt`: instalamos directamente `mcp`, `ib_async`,
> `python-dotenv`, `pytest` (IBKR) y `pypdf`, `fastembed`, `chromadb`
> (Research), pero el archivo tiene muchas más líneas porque `pip freeze`
> incluye también las dependencias de esos paquetes. Es normal — no
> significa que el proyecto use todas esas librerías directamente.

> Nota: en sistemas Debian/Ubuntu, `python3 -m venv` por sí solo puede
> fallar si falta el paquete de sistema `python3-venv` (se instala con
> `sudo apt install python3-venv`). Si no tienes acceso a `sudo`, la
> combinación `--without-pip` + instalar `pip` a mano dentro del venv (los
> pasos de arriba) da un entorno igual de aislado sin necesitar privilegios
> de administrador.

## 6. Conexión con IBKR (Paper Trading)

1. Instala **IB Gateway** (descarga oficial, `stable-standalone`):
   ```bash
   curl -o ibgateway-stable-standalone-linux-x64.sh \
     https://download.interactivebrokers.com/installers/ibgateway/stable-standalone/ibgateway-stable-standalone-linux-x64.sh
   chmod u+x ibgateway-stable-standalone-linux-x64.sh
   ./ibgateway-stable-standalone-linux-x64.sh
   ```
2. Ábrelo e inicia sesión seleccionando explícitamente **"Paper Trading"**
   (no "Live Trading"), con tu usuario/contraseña de Paper Trading.
3. Verifica que el puerto de la API sea **4002** (Paper). Puedes confirmarlo así:
   ```bash
   ss -ltnp | grep 4002   # deberia aparecer un proceso "java" escuchando
   ```
4. Copia `.env.example` a `.env` (si no lo has hecho) y ajusta los valores
   si tu configuración es distinta.

**Por qué esto es seguro:** `src/config/settings.py` **rechaza arrancar**
si `IBKR_PORT` no es exactamente `4002`, y también si
`IBKR_PAPER_TRADING_CONFIRMED` no dice `true`. Además, la conexión
(`src/ibkr/connection.py`) se abre con `readonly=True`, que hace que IB
Gateway rechace cualquier intento de enviar órdenes a nivel de API, incluso
antes de que existan herramientas de órdenes.

## 7. Configuración de Claude Code

El servidor está registrado en `.mcp.json` (raíz del proyecto), con scope
`project`. Esto significa que el archivo se versiona en Git y cualquiera que
abra este repo en Claude Code verá el servidor propuesto — pero **no se
ejecuta automáticamente**: Claude Code lo marca como "Pending approval"
hasta que abras una sesión dentro de esta carpeta y lo apruebes.

```bash
cd ~/mcp-ibkr
claude    # al iniciar, Claude Code te preguntará si confías en mcp-ibkr
```

Para verificar el estado del servidor en cualquier momento:

```bash
claude mcp list
claude mcp get mcp-ibkr
```

Si alguna vez quieres quitarlo:

```bash
claude mcp remove mcp-ibkr -s project
```

## 8. Herramientas de IBKR

| Herramienta | Categoría | Riesgo | Estado | Descripción |
|---|---|---|---|---|
| `verificar_conexion_ibkr` | `account` | READ_ONLY | Activa | Confirma que hay conexión activa con IB Gateway (Paper Trading) y lista las cuentas visibles. |
| `consultar_resumen_cuenta` | `account` | READ_ONLY | Activa | Valor neto, efectivo disponible, buying power y margen. |
| `consultar_precio_mercado` | `market` | READ_ONLY | Activa | Último precio, bid/ask, cierre anterior y volumen de una acción. |
| `consultar_datos_historicos` | `market` | READ_ONLY | Activa | Velas OHLCV históricas de una acción. |
| `consultar_posiciones` | `positions` | READ_ONLY | Activa | Posiciones abiertas (todas o filtradas por símbolo). |
| `consultar_pnl` | `positions` | READ_ONLY | Activa | P&L diario, no realizado y realizado de la cuenta. |
| `consultar_ordenes` | `orders` | READ_ONLY | Activa | Lista órdenes abiertas y su estado. |
| `crear_orden` | `orders` | **ACTION** | **Deshabilitada** | Crea una orden MKT/LMT. Requiere doble activación (ver sección 12). |
| `cancelar_orden` | `orders` | **ACTION** | **Deshabilitada** | Cancela una orden abierta por `orderId`. Requiere doble activación. |

## 9. Research: documentos provistos por el usuario

### 9.1. Qué es y qué NO es

Research es un sistema de ingestion + RAG (retrieval-augmented generation)
sobre documentos que **vos** le entregás al sistema. El punto de entrada
siempre es un archivo local que ya tenés.

**Research nunca:**
- busca papers en Internet, arXiv, Google Scholar, Semantic Scholar, etc.
- descubre ni descarga literatura por su cuenta.
- ejecuta ninguna acción sobre IBKR (ni siquiera de lectura) — es un
  dominio completamente aislado (ver sección 2).
- inventa metadata: si un campo (autor, DOI, fecha de publicación) no está
  presente en el documento, queda como desconocido, nunca se completa
  adivinando o buscando afuera.

**Research sí:**
- extrae texto, metadata propia del archivo (título/autor/keywords de un
  PDF, DOI/arXiv ID si aparecen impresos en el texto), lo divide en chunks
  preservando página/sección, genera embeddings, lo indexa, y permite
  buscarlo semánticamente con trazabilidad completa hacia la fuente.

### 9.2. Pipeline de ingestion

```
Documento provisto por el usuario (PDF)
      │
      v
DocumentLoader        -- lee bytes, detecta tipo, calcula hash sha256
      │
      v
DocumentParser         -- extrae texto por pagina (PdfParser / pypdf)
      │
      v
TextNormalizer          -- corrige guiones de corte de linea, saltos de linea
      │
      v
MetadataExtractor        -- /Info del PDF + DOI/arXiv ID literales del texto
      │
      v
StructuralChunker          -- chunks respetando parrafo/pagina/seccion
      │
      v
EmbeddingProvider (fastembed) -- vectores locales, sin API key
      │
      v
VectorStore (Chroma) + DocumentStore (sqlite)
      │
      v
research_search / document://{id}
```

`IngestionService` (`src/research/services/ingestion_service.py`) es el
único lugar que conoce este orden completo. Es idempotente por hash: si
volvés a ingerir el mismo archivo, se rechaza con un mensaje claro salvo
que pases `force_reindex=true`.

### 9.3. Formatos soportados

Por ahora, **solo PDF**. La arquitectura ya tiene la abstracción
(`DocumentType`, `DocumentParser`, `MetadataExtractor`) para agregar TXT,
Markdown o DOCX más adelante sin rediseñar nada — se agregan detrás de la
misma interfaz cuando haga falta.

### 9.4. Trazabilidad y citas

Cada resultado de búsqueda (`ResearchResult`) incluye `document_id`,
`chunk_id`, página y sección (cuando se pudieron detectar), score de
similitud, y el título/filename del documento de origen. La detección de
sección es una heurística best-effort (encabezados numerados o nombres de
sección habituales en papers como "Introduction", "Results"); cuando no
hay certeza razonable, queda en `None` en vez de adivinar.

### 9.5. Configuración

Todas las variables son opcionales — Research arranca con valores por
defecto razonables sin tocar `.env` (a diferencia de IBKR, no maneja
dinero real, así que no hay ningún interruptor de seguridad que confirmar):

| Variable | Default | Qué controla |
|---|---|---|
| `RESEARCH_STORAGE_PATH` | `data/research` | Dónde vive el índice vectorial y el registro de documentos (versionado — ver 9.8). |
| `RESEARCH_EMBEDDING_MODEL` | `BAAI/bge-small-en-v1.5` | Modelo de embeddings local (fastembed). |
| `RESEARCH_CHUNK_SIZE` | `1000` | Tamaño máximo de chunk, en caracteres. |
| `RESEARCH_CHUNK_OVERLAP` | `150` | Superposición entre chunks vecinos (debe ser menor que `RESEARCH_CHUNK_SIZE`). |
| `RESEARCH_RETRIEVAL_TOP_K` | `5` | Resultados devueltos por defecto en `research_search`. |
| `RESEARCH_SCORE_THRESHOLD` | `0.0` | Score mínimo (0-1) para considerar un resultado relevante. |

### 9.6. Herramientas y recursos

| Nombre | Tipo | Riesgo | Descripción |
|---|---|---|---|
| `research_ingest_document` | Tool | READ_ONLY* | Ingiere un PDF desde una ruta local. `path`, `force_reindex` opcional. |
| `research_list_documents` | Tool | READ_ONLY | Lista los documentos ya ingeridos. |
| `research_get_document` | Tool | READ_ONLY | Metadata completa de un documento por `document_id`. |
| `research_search` | Tool | READ_ONLY | Búsqueda semántica sobre los documentos ya ingeridos. `query`, `top_k`, `document_id`, `score_threshold` opcionales. |
| `document://{document_id}` | Resource | READ_ONLY | Contenido completo (reconstruido) de un documento, para leerlo directamente en vez de buscarlo. |

\* `research_ingest_document` usa `RiskLevel.READ_ONLY` porque no tiene
ningún riesgo financiero (nunca toca IBKR), pero sus `ToolAnnotations`
marcan `readOnlyHint=False` porque sí escribe en el índice local — son dos
conceptos distintos que conviene no confundir (ver `src/utils/risk.py` vs
`mcp.types.ToolAnnotations`).

No existe una tool `research_get_metadata` separada: `research_get_document`
ya cubre ese caso, y no se agregan herramientas solo para sumar cantidad.

### 9.7. Roadmap de Research (no implementado todavía)

- Más formatos de documento (TXT, Markdown, DOCX) detrás de la misma
  interfaz `DocumentParser`.
- `research_compare_documents` / `research_extract_strategy`: análisis
  cruzado entre documentos y extracción de parámetros de estrategia
  (lookback, rebalanceo, universo, etc.), con sus citas correspondientes.
- Una capa de orquestación/aplicación que combine evidencia de Research
  con datos de mercado de IBKR para reproducir análisis — sin que ninguno
  de los dos dominios dependa del otro directamente (ver sección 2).
- Búsqueda híbrida (semántica + palabra clave) y filtrado por
  autor/fecha/tags en `Retriever`.

### 9.8. `data/research/` versionado, `papers/` no

`data/research/` (índice `vectors/` de Chroma + `documents.db`) **sí se
sube al repo**, para que al clonar el proyecto el MCP ya tenga el
conocimiento indexado sin depender de que cada persona vuelva a conseguir
los mismos 14 papers. `papers/*.pdf` (los PDFs originales) **nunca se
sube** — siguen gitignored.

Para poder versionar el índice sin redistribuir el texto de papers con
copyright (NBER, Journal of Finance, RFS, JFR, Fed), el contenido de cada
chunk en `vectors/` no es el texto extraído literal del PDF: es una nota
parafraseada, generada con Claude (`scripts/paraphrase_research_chunks.py`),
que preserva hallazgos/cifras/metodología pero está redactada con palabras
y estructura propias — no reutiliza más de 5 palabras consecutivas del
original (nombres propios, símbolos de fórmulas y términos técnicos
cortos quedan exentos). Los embeddings de `vectors/` se calcularon sobre
ese texto parafraseado, no sobre el original.

`data/research/_original_chunks_backup.json` guarda el texto original
verbatim de los 2300 chunks (para poder re-parafrasear sin volver a leer
los PDFs) y está explícitamente gitignored — nunca debe subirse.

Si volvés a ingerir un PDF nuevo con `research_ingest_document`, el chunk
que se guarda en Chroma va a ser el texto original (verbatim), no una
versión parafraseada — `IngestionService` no llama a
`scripts/paraphrase_research_chunks.py` automáticamente. Si vas a subir
ese documento nuevo al repo público, corré el script antes de hacer commit
de `data/research/`.

## 10. Cómo agregar / modificar / eliminar / desactivar una herramienta

Cada herramienta es **un archivo** dentro de `src/tools/<categoria>/`, con
esta forma (ver `src/tools/account/verificar_conexion_ibkr.py` como ejemplo real):

```python
from mcp.types import ToolAnnotations
from src.utils.risk import RiskLevel

NAME = "mi_herramienta"
DESCRIPTION = "Que hace, cuando usarla, que devuelve, si modifica la cuenta."
ANNOTATIONS = ToolAnnotations(readOnlyHint=True, openWorldHint=True)
ENABLED = True
RISK_LEVEL = RiskLevel.READ_ONLY  # o RiskLevel.ACTION si modifica algo en IBKR

def mi_herramienta(parametro: str) -> str:
    return "resultado"
```

Esta convención es la misma para las categorías de IBKR (`account`,
`market`, `positions`, `orders`) y para `research`.

La función debe llamarse **igual que `NAME`** — así el registro central
(`src/tools/registry.py`) la encuentra automáticamente. Si `RISK_LEVEL` es
`ACTION`, además de `ENABLED = True` hace falta `IBKR_ENABLE_ACTION_TOOLS=true`
en `.env` (ver sección 12) — dos llaves independientes, a propósito. Esta
llave es específica de acciones sobre IBKR: las herramientas de Research
siempre son `RiskLevel.READ_ONLY`.

Sobre `ANNOTATIONS`: `destructiveHint` e `idempotentHint` solo son
significativos cuando `readOnlyHint=False` (así lo documenta la spec de
MCP) — por eso en una herramienta de solo lectura alcanza con `readOnlyHint`
y `openWorldHint`. Solo agrégalos si `readOnlyHint=False`, como en
`src/tools/orders/crear_orden.py` o `src/tools/research/research_ingest_document.py`.

### Agregar una herramienta
1. Crear el archivo en la categoría correspondiente (o crear una categoría
   nueva, ver más abajo).
2. Agregar el nombre del archivo (sin `.py`) a la lista `TOOLS` en el
   `__init__.py` de esa categoría.
3. Reiniciar la sesión de Claude Code para que la recoja (Claude Code lee
   las herramientas una sola vez, al arrancar el servidor; `claude mcp list`
   solo consulta el estado de la conexión, no recarga nada).

### Modificar una herramienta
Editar directamente su archivo — `DESCRIPTION`, parámetros de la función,
lógica interna, etc. No hay que tocar `registry.py`.

### Eliminar una herramienta
Borrar el archivo y quitar su nombre de `TOOLS` en el `__init__.py` de su
categoría.

### Desactivar una herramienta (sin borrarla)
Poner `ENABLED = False` en su archivo. `registry.py` la salta automáticamente.

### Crear una categoría nueva
Crear la carpeta `src/tools/<categoria>/` con un `__init__.py` que defina
`TOOLS: list[str] = [...]`, y agregar el nombre de la categoría a
`CATEGORIES` en `src/tools/registry.py`.

### Agregar un MCP Resource
Mismo principio, pero en `src/resources/`: crear el archivo con `URI`,
`NAME`, `DESCRIPTION`, `ENABLED` y una función con el mismo nombre que
`NAME` cuyos parámetros coincidan con las variables de la URI (ver
`src/resources/documents.py`), y agregar el nombre del módulo a
`RESOURCES` en `src/resources/registry.py`.

## 11. Testing

```bash
cd ~/mcp-ibkr
.venv/bin/python3 -m pytest tests/ -v
```

**IBKR:**
- `tests/test_server.py` — el servidor arranca y expone las herramientas
  esperadas (lo mismo que vería Claude Code al conectarse); las
  herramientas de solo lectura no fijan anotaciones que no aplican; y
  cada herramienta responde con un mensaje amigable si IBKR no está
  disponible, en vez de dejar escapar una excepción.
- `tests/test_settings.py` — la configuración **rechaza** puerto distinto de
  4002 y la falta de `IBKR_PAPER_TRADING_CONFIRMED`.
- `tests/test_connection.py` — la capa de conexión serializa los intentos
  de conectar (con IBKR simulado, no requiere Gateway real): si dos
  herramientas se llaman casi al mismo tiempo, nunca hay dos intentos de
  conexión corriendo en paralelo.
- `tests/test_risk_system.py` — las herramientas `ACTION` (`crear_orden`,
  `cancelar_orden`) **no se registran** por defecto, y las validaciones de
  una orden (cantidad, tipo, precio) rechazan parámetros inválidos.
- `tests/test_ibkr_integration.py` — conexión real contra IB Gateway. Si
  Gateway no está corriendo, este test se **salta** (no falla) — es
  esperado, no es un error del proyecto.

Todas las pruebas relacionadas con órdenes usan `validar_parametros_orden`
de forma aislada (sin tocar IBKR) o dependen de que `crear_orden` esté
deshabilitada por defecto: **ningún test de este proyecto envía una orden
real**, ni siquiera en Paper Trading.

**Research** (un archivo de test por componente del pipeline):
`test_research_models.py`, `test_research_ingestion.py`,
`test_research_normalizer.py`, `test_research_metadata.py`,
`test_research_chunker.py`, `test_research_embeddings.py`,
`test_research_vector_store.py`, `test_research_document_store.py`,
`test_research_ingestion_service.py`, `test_research_retriever.py`,
`test_research_service.py`, `test_research_mcp_tools.py` (integración MCP:
las 4 tools registradas y funcionando de punta a punta) y
`test_research_resources.py` (integración del resource `document://`).

Ningún test de Research descarga un modelo real ni toca la carpeta real
`data/research/`: todos reemplazan `fastembed.TextEmbedding` por un fake
determinista (`tests/support/fake_embeddings.py`) y usan directorios
temporales (`tmp_path` / `RESEARCH_STORAGE_PATH` sobreescrita). Los PDFs de
prueba se generan a mano, byte a byte (`tests/support/pdf_fixtures.py`),
sin depender de una librería de generación de PDF.

## 12. De Paper Trading a Live Trading

> ⚠️ **Live Trading no está soportado por este proyecto todavía, y
> `crear_orden`/`cancelar_orden` están deshabilitadas por defecto.**
> Lo que sigue es la explicación de las capas de seguridad, no una
> invitación a activarlas sin pensarlo.

Hay **tres capas independientes** que impiden que se envíe una orden real por accidente:

1. **Puerto obligatorio 4002** — `src/config/settings.py` rechaza arrancar
   si `IBKR_PORT` no es exactamente el de Paper Trading. Esta versión del
   proyecto **no tiene ningún camino de código para usar el puerto 4001**
   (Live).
2. **Doble llave para herramientas ACTION** — `crear_orden` y
   `cancelar_orden` necesitan `ENABLED = True` en su propio archivo **y**
   `IBKR_ENABLE_ACTION_TOOLS=true` en `.env`. Ninguna de las dos está
   activada por defecto. Ambas se leen solo al arrancar el servidor: si las
   cambias con el servidor ya corriendo, necesitas reiniciar la sesión de
   Claude Code para que el cambio tenga efecto.
3. **Conexión de solo lectura a nivel de API** — mientras
   `IBKR_ENABLE_ACTION_TOOLS` sea `false`, `src/ibkr/connection.py` se
   conecta con `readonly=True`: IB Gateway rechaza cualquier orden aunque
   alguien lograra saltarse las dos capas anteriores.

Si en el futuro decides habilitar el envío de órdenes **en Paper Trading**,
el camino sería: revisar y reforzar las validaciones de
`src/tools/orders/crear_orden.py`, poner `IBKR_ENABLE_ACTION_TOOLS=true` en
`.env`, y `ENABLED = True` en los archivos de orden. Soporte para **Live
Trading real no está implementado ni planeado en este proyecto** — haría
falta una revisión de seguridad completamente aparte antes de considerarlo.

**Research nunca participa de este camino.** Ningún archivo de
`src/research/`, `src/tools/research/` o `src/resources/` importa `ib_async`
ni `src/ibkr/`, y ninguna herramienta de Research puede llegar a ejecutar
una orden ni siquiera indirectamente: en el peor caso, devuelve texto o
metadata de un documento.

## 13. Estado actual

**IBKR:**
- [x] Fase 1 — Arquitectura y conceptos
- [x] Fase 2 — Estructura mínima del proyecto
- [x] Fase 3 — Entorno de desarrollo
- [x] Fase 4 — Servidor MCP mínimo
- [x] Fase 5 — Conectar Claude Code con el MCP
- [x] Fase 6 — Primera herramienta de prueba
- [x] Fase 7 — Conexión con IB Gateway (Paper Trading)
- [x] Fase 8 — Herramientas de consulta
- [x] Fase 9 — Sistema de validación y riesgo
- [x] Fase 10 — Herramientas de órdenes (bloqueadas)
- [x] Fase 11 — Testing completo
- [x] Fase 12 — Documentación final

**Research:**
- [x] Fase 1 — Modelos de dominio (Document, Chunk, ResearchResult)
- [x] Fase 2 — Document loader + PDF parser
- [x] Fase 3 — Normalizer + metadata extractor
- [x] Fase 4 — Chunker estructural (página/sección)
- [x] Fase 5 — Embedding provider (fastembed)
- [x] Fase 6 — Vector store (Chroma)
- [x] Fase 7 — Document store (sqlite, dedup por hash)
- [x] Fase 8 — Ingestion service (orquestación del pipeline)
- [x] Fase 9 — Retrieval service con citas
- [x] Fase 10 — MCP tools de Research
- [x] Fase 11 — MCP Resources (`document://`)
- [ ] Más formatos (TXT/Markdown/DOCX), análisis cruzado entre documentos,
      e integración con datos de IBKR — ver sección 9.7 (Roadmap).