alexgcherk/InteractiveBrokers_ClientPortal_WebAPI

GitHub: alexgcherk/InteractiveBrokers_ClientPortal_WebAPI

为 Interactive Brokers Client Portal Gateway 提供强类型、异步优先的 .NET 8 客户端,封装交易、行情、账户、警报等全流程操作。

Stars: 0 | Forks: 0

# IB ClientPortal WebAPI 客户端 [![NuGet](https://img.shields.io/nuget/v/IB.ClientPortal.Client.svg)](https://www.nuget.org/packages/IB.ClientPortal.Client) [![NuGet 下载量](https://img.shields.io/nuget/dt/IB.ClientPortal.Client.svg)](https://www.nuget.org/packages/IB.ClientPortal.Client) [![CI](https://static.pigsec.cn/wp-content/uploads/repos/2026/03/87ac88a594012236.svg)](https://github.com/alexgcherk/InteractiveBrokers_ClientPortal_WebAPI/actions/workflows/ci.yml) [![许可证: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) 一个用于 [Interactive Brokers Client Portal Web API](https://www.interactivebrokers.com/en/trading/ib-api.php)(也称为 **IB Gateway**)的强类型 .NET 8 客户端库。它将每个端点封装在强类型、异步优先的接口中,使您无需手动构建 HTTP 调用即可进行交易、查询持仓、流式传输市场数据和管理警报。 ``` dotnet add package IB.ClientPortal.Client ``` ## 目录 - [架构](#architecture) - [前置条件](#prerequisites) - [项目结构](#project-structure) - [配置](#configuration) - [快速开始](#quick-start) - [认证流程](#authentication-flow) - [客户端参考](#client-reference) - [认证](#auth) - [账户](#account) - [订单](#orders) - [投资组合](#portfolio) - [市场数据](#market-data) - [合约](#contracts) - [警报](#alerts) - [绩效分析](#performance-analytics) - [监视列表](#watchlists) - [下单流程](#order-placement-flow) - [示例](#examples) - [安全性](#security) - [测试](#testing) ## 架构 该解决方案围绕单个 `IBPortalClient` 构建,每个域公开一个类型化客户端。所有客户端共享一个带有持久 `CookieContainer` 的 `HttpClient` 实例,因此会话 cookie(滚动的 `x-sess-uuid`)会被自动处理。 ``` graph TD App["Your Application"] Client["IBPortalClient"] Http["IBPortalHttpClient\n(shared HttpClient + CookieContainer)"] Gateway["IB Client Portal Gateway\nhttps://localhost:5000/v1/api/"] App --> Client Client --> Auth["AuthClient"] Client --> Account["AccountClient"] Client --> Orders["OrderClient"] Client --> Portfolio["PortfolioClient"] Client --> MarketData["MarketDataClient"] Client --> Contracts["ContractClient"] Client --> Alerts["AlertClient"] Client --> Performance["PerformanceClient"] Client --> Watchlists["WatchlistClient"] Client --> Fyi["FyiClient (generated)"] Client --> Calendar["CalendarClient (generated)"] Auth --> Http Account --> Http Orders --> Http Portfolio --> Http MarketData --> Http Contracts --> Http Alerts --> Http Performance --> Http Watchlists --> Http Http -->|"HTTPS + cookies"| Gateway ``` ## 前置条件 | 需求 | 版本 | |---|---| | [.NET SDK](https://dotnet.microsoft.com/download) | 8.0+ | | [IB Client Portal Gateway](https://www.interactivebrokers.com/en/trading/ib-api.php) | 最新版 | | Interactive Brokers 账户 | 模拟或实盘 | | [Newtonsoft.Json](https://www.nuget.org/packages/Newtonsoft.Json) | 13.0.3 *(唯一的运行时依赖项)* | 在进行任何调用之前,Gateway 必须在本地运行。它使用自签名 TLS 证书 —— 库会自动处理 `localhost` 的此问题(请参阅 [安全性](#security))。 ## 项目结构 ``` InteractiveBrokers_ClientPortal_WebAPI/ ├── IB.ClientPortal.Client/ # Library — the client you reference │ ├── IBPortalClient.cs # Main entry point │ ├── IBPortalClientOptions.cs # Configuration │ ├── IBPortalHttpClient.cs # Low-level HTTP wrapper │ ├── Clients/ # One file per domain │ │ ├── AuthClient.cs │ │ ├── AccountClient.cs │ │ ├── OrderClient.cs │ │ ├── PortfolioClient.cs │ │ ├── MarketDataClient.cs │ │ ├── ContractClient.cs │ │ ├── AlertClient.cs │ │ ├── PerformanceClient.cs │ │ └── WatchlistClient.cs │ ├── Models/ # Strongly-typed request/response models │ └── Generated/ # NSwag-generated FYI and Calendar clients │ ├── IB.ClientPortal.Client.UnitTests/ # Fast, offline unit tests (Moq + NUnit) │ ├── Clients/ # Per-domain test files │ │ └── SecurityTests.cs # SSL bypass, URL encoding, exception handling │ └── MockHttpHandler.cs # Shared mock infrastructure │ └── IB.ClientPortal.IntegrationTests/ # Live integration tests (require running gateway) ├── GlobalSetup.cs # Assembly-level setup; creates shared client ├── GatewaySettings.cs # Config model ├── appsettings.integration.json # Non-secret defaults (committed) └── appsettings.integration.local.json # ⚠ Private values (gitignored) ``` ## 配置 ### IBPortalClientOptions | 属性 | 类型 | 默认值 | 描述 | |---|---|---|---| | `BaseUrl` | `string` | `"https://localhost:5000"` | Gateway 基础 URL | | `AccountId` | `string?` | `null` | 用于账户范围调用的默认账户 ID | | `IgnoreSslErrors` | `bool` | `false` | 跳过 TLS 验证。**对于 localhost 不需要** —— 库会自动为环回地址跳过验证 | | `RequestTimeout` | `TimeSpan` | 15 秒 | 每次请求的 HTTP 超时时间 | | `SessionCookie` | `string?` | `null` | 预置 `x-sess-uuid` cookie(高级 —— 请参阅 [认证流程](#authentication-flow)) | ### 集成测试机密 将您的真实凭据复制到 gitignore 的本地覆盖文件中: ``` // IB.ClientPortal.IntegrationTests/appsettings.integration.local.json (gitignored) { "Gateway": { "BaseUrl": "https://localhost:5000", "AccountId": "DU1234567", "IgnoreSslErrors": false, "RequestTimeoutSeconds": 30, "SessionCookie": "" // optional — leave empty for automatic acquisition } } ``` ## 快速开始 ``` using IB.ClientPortal.Client; // 1. Create the client var options = new IBPortalClientOptions { BaseUrl = "https://localhost:5000", AccountId = "DU1234567" // IgnoreSslErrors is false by default; localhost is bypassed automatically }; using var client = new IBPortalClient(options); // 2. Verify gateway is alive var alive = await client.Auth.PingAsync(); Console.WriteLine($"Gateway alive: {alive}"); // 3. Authenticate var status = await client.Auth.GetStatusAsync(); if (status?.Authenticated != true) { await client.Auth.InitSsoDhAsync(); await Task.Delay(2000); } // 4. REQUIRED before any order operations var accounts = await client.Account.GetAccountsAsync(); Console.WriteLine($"Accounts: {string.Join(", ", accounts!.Accounts!)}"); // 5. Keep the session alive every ~5 minutes await client.Auth.TickleAsync(); ``` ## 认证流程 IB Gateway 使用基于 Web 的 SSO 会话。客户端必须在任何交易或市场数据调用之前进行认证,并且必须至少每 5 分钟发送一次保活(`/tickle`),否则会话将过期。 ``` sequenceDiagram participant App participant Client as IBPortalClient participant GW as IB Gateway App->>Client: PingAsync() Client->>GW: GET /sso/ping GW-->>Client: "true" / error Client-->>App: bool (is alive) App->>Client: GetStatusAsync() Client->>GW: GET /iserver/auth/status GW-->>Client: AuthStatus Client-->>App: AuthStatus alt Not authenticated App->>Client: InitSsoDhAsync() Client->>GW: POST /iserver/auth/ssodh/init GW-->>Client: AuthStatus (authenticated=true) Client-->>App: AuthStatus end loop Every ≤ 5 minutes App->>Client: TickleAsync() Client->>GW: GET /tickle GW-->>Client: TickleResponse (ssoExpires countdown) Client-->>App: TickleResponse end App->>Client: LogoutAsync() Client->>GW: GET /logout GW-->>Client: ok ``` ## 客户端参考 ### 认证 (Auth) | 方法 | 描述 | |---|---| | `PingAsync()` | 检查 Gateway 进程是否可达 | | `GetStatusAsync()` | 返回当前的经纪会话状态(`Authenticated`、`Established`) | | `InitSsoDhAsync()` | 打开(或重新打开)经纪会话 | | `ReauthenticateAsync()` | 强制重新认证 | | `TickleAsync()` | 保活 —— 至少每 5 分钟调用一次 | | `LogoutAsync()` | 结束当前会话 | ### 账户 (Account) | 方法 | 描述 | |---|---| | `GetAccountsAsync()` | 检索账户列表并设置交易上下文。**必须在订单操作之前调用。** | | `GetSummaryAsync(accountId)` | 完整的账户摘要:购买力、保证金、余额 | | `GetPnlAsync()` | 按账户/细分的每日盈亏 (PnL) | | `GetUserAsync()` | 当前用户信息和功能标志 | | `GetMtaAsync()` | Mobile Trading Assistant 警报数据 | | `SwitchAccountAsync(accountId)` | 切换活动账户 | | `GetCurrencyPairsAsync(currency)` | 基础货币(默认为 `"USD"`)的所有货币对 | | `GetExchangeRateAsync(source, target)` | 两种货币之间的即期汇率 | | `GetSignaturesAndOwnersAsync(accountId)` | 账户上的申请人姓名 | ### 订单 (Orders) | 方法 | 描述 | |---|---| | `GetOrdersAsync(filters?)` | 活动订单;可选按状态筛选(`"filled,cancelled"`) | | `GetTradesAsync(days?)` | 今日已成交交易;可选回溯 N 天 | | `PlaceOrdersAsync(accountId, orders[])` | 下单一个或多个订单 | | `WhatIfAsync(accountId, orders[])` | 模拟订单影响(保证金/佣金)而不实际下单 | | `ModifyOrderAsync(accountId, orderId, order)` | 修改现有订单 | | `CancelOrderAsync(accountId, orderId)` | 取消订单;`orderId = -1` 取消所有订单 | | `GetOrderStatusAsync(orderId)` | 单个订单的详细状态 | | `ReplyAsync(replyId, confirmed)` | 确认待处理的订单警告(必须在警告后立即调用) | | `SuppressMessagesAsync(messageIds[])` | 预先抑制已知的警告代码(例如 `"o163"`) | | `ResetSuppressedMessagesAsync()` | 清除所有已抑制的消息 | ### 投资组合 (Portfolio) | 方法 | 描述 | |---|---| | `GetAccountsAsync()` | 所有账户及其完整元数据 | | `GetSubAccountsAsync()` | 子账户列表 | | `GetSubAccountsAsync(page)` | 分页子账户(适用于拥有 >100 个账户的顾问) | | `GetPositionsAsync(accountId, page)` | 持仓页面(基于 0) | | `GetFirstPositionsAsync(accountId)` | 获取第一页持仓的快捷方式 | | `GetPositionByConidAsync(accountId, conid)` | 特定合约的持仓 | | `InvalidatePositionCacheAsync(accountId)` | 使服务器端持仓缓存失效 | | `GetAllocationAsync(accountId)` | 资产类别、板块和组分配明细 | | `GetLedgerAsync(accountId)` | 每种货币的现金余额 | | `GetMetaAsync(accountId)` | 完整的账户元数据 | ### 市场数据 (Market Data) | 方法 | 描述 | |---|---| | `GetSnapshotAsync(conids, fields)` | 实时快照(首次调用订阅;第二次返回数据) | | `GetAltSnapshotAsync(conids, fields)` | 通过 `/md/snapshot` 的替代快照端点 | | `GetRegSnapshotAsync(conid)` | 监管快照 —— ⚠ 每次调用费用 $0.01 | | `GetHistoryAsync(conid, period, bar, exchange?, outsideRth)` | 历史 OHLCV 柱状图 | | `UnsubscribeAllAsync()` | 取消所有活动的市场数据订阅 | | `UnsubscribeAsync(conid)` | 取消单个合约的订阅 | | `GetScannerParamsAsync()` | 完整扫描器配置(缓存 15 分钟) | | `RunScannerAsync(request)` | 执行市场扫描器 | **常用字段代码**(通过 `MarketDataFields`): ``` MarketDataFields.Last // "31" — last traded price MarketDataFields.Bid // "84" MarketDataFields.Ask // "86" MarketDataFields.Volume // "87" MarketDataFields.EquityDefault // pre-built set: Last, Bid, Ask, Size, Volume, OHLC, Change ``` ### 合约 (Contracts) | 方法 | 描述 | |---|---| | `SearchAsync(symbol, secType?, nameSearch)` | 按代码搜索合约 | | `GetInfoAsync(conid)` | 合约详情 | | `GetInfoAndRulesAsync(conid)` | 合约信息及订单规则 | | `GetRulesAsync(request)` | 特定合约和买卖方向的订单规则 | | `GetStrikesAsync(conid, month, secType, exchange?)` | 期权行权价列表 | | `GetSecDefInfoAsync(conid, secType, month?, strike?, right?)` | 衍生品 secdef | | `GetSecDefAsync(conids)` | 包含交易规则的完整 secdef | | `GetStocksAsync(symbols)` | 按代码获取股票合约 | | `GetFuturesAsync(symbols)` | 未过期的期货合约 | | `GetAllConidsAsync(exchange, assetClass)` | 交易所上的所有合约 ID | | `GetTradingScheduleAsync(assetClass, symbol, exchange?)` | 每个场所的交易时间表 | | `GetContractTradingScheduleAsync(conid, exchange?)` | 包含 epoch 时间的 6 天时间表 | | `GetAlgosAsync(conid, algos?, addDescription, addParams)` | IB Algo 策略 | | `GetBondFiltersAsync(symbol, issuerId)` | 债券搜索筛选选项 | ### 警报 (Alerts) | 方法 | 描述 | |---|---| | `GetAlertsAsync(accountId)` | 列出账户的所有警报 | | `GetAlertAsync(orderId)` | 特定警报的完整详情 | | `CreateAlertAsync(accountId, request)` | 创建或更新警报 | | `SetAlertActiveAsync(accountId, alertId, active)` | 激活或停用警报 | | `DeleteAlertAsync(accountId, alertId)` | 删除警报;`alertId = 0` 删除所有警报 | | `GetMtaAlertAsync()` | Mobile Trading Assistant 警报 | ### 绩效分析 (Performance Analytics) | 方法 | 描述 | |---|---| | `GetPerformanceAsync(accountIds[], period)` | 投资组合绩效时间序列。周期:`"1M"`、`"3M"`、`"6M"`、`"1Y"`、`"2Y"`、`"3Y"`、`"5Y"`、`"MTD"`、`"YTD"` | | `GetSummaryAsync(accountIds[])` | 按资产类别的投资组合摘要 | | `GetTransactionsAsync(accountIds[], conids[], currency)` | 特定合约的交易历史 | ### 监视列表 (Watchlists) | 方法 | 描述 | |---|---| | `GetWatchlistsAsync()` | 所有监视列表(系统和用户定义) | | `CreateWatchlistAsync(id, name, conids[])` | 创建监视列表 | | `GetWatchlistAsync(id)` | 获取特定监视列表 | | `DeleteWatchlistAsync(id)` | 删除监视列表 | ## 下单流程 下单可能会返回 **直接确认** 或需要立即回复的 **警告**。客户端处理这两种响应形式。 ``` sequenceDiagram participant App participant Client as IBPortalClient participant GW as IB Gateway Note over App,GW: REQUIRED: call GetAccountsAsync() before any order operations App->>Client: GetAccountsAsync() Client->>GW: GET /iserver/accounts GW-->>Client: AccountsResponse Client-->>App: AccountsResponse App->>Client: PlaceOrdersAsync(accountId, orders[]) Client->>GW: POST /iserver/account/{accountId}/orders alt Direct confirmation GW-->>Client: [{ order_id, order_status }] Client-->>App: PlaceOrderResponse[] (OrderId set) else Warning requires confirmation GW-->>Client: { id: "replyId", message: ["warning text"] } Client-->>App: PlaceOrderResponse[] (ReplyId set, check Message) App->>Client: ReplyAsync(replyId, confirmed: true) Client->>GW: POST /iserver/reply/{replyId} GW-->>Client: [{ order_id, order_status }] Client-->>App: PlaceOrderResponse[] (OrderId set) end ``` ## 示例 ### 下限价单 ``` // Pre-flight await client.Account.GetAccountsAsync(); // Suppress common warnings so PlaceOrdersAsync confirms directly await client.Orders.SuppressMessagesAsync(["o163", "o354"]); var order = new PlaceOrderBody { AccountId = "DU1234567", Conid = 265598, // AAPL contract ID SecType = "265598:STK", Side = "BUY", OrderType = "LMT", Price = 195.00, Quantity = 10, Tif = "DAY" }; var responses = await client.Orders.PlaceOrdersAsync("DU1234567", [order]); foreach (var r in responses!) { if (r.ReplyId is not null) { Console.WriteLine($"Warning: {string.Join("; ", r.Message!)}"); // Confirm the warning immediately — before any other request await client.Orders.ReplyAsync(r.ReplyId, confirmed: true); } else { Console.WriteLine($"Order placed: {r.OrderId} — {r.OrderStatus}"); } } ``` ### 模拟订单 (What-If) ``` var whatIf = await client.Orders.WhatIfAsync("DU1234567", [order]); Console.WriteLine($"Estimated commission: {whatIf!.Amount?.Change}"); Console.WriteLine($"Initial margin impact: {whatIf.Initial?.Change}"); ``` ### 实时市场数据快照 ``` // First call subscribes; poll until data is returned string conids = "265598,8314"; // AAPL, IBM string fields = MarketDataFields.EquityDefault; MarketDataSnapshot[]? snapshot = null; for (int i = 0; i < 5 && snapshot is null or { Length: 0 }; i++) { snapshot = await client.MarketData.GetSnapshotAsync(conids, fields); if (snapshot?.Length == 0) await Task.Delay(500); } foreach (var s in snapshot!) Console.WriteLine($"Conid {s.Conid}: last={s[MarketDataFields.Last]}"); // Clean up await client.MarketData.UnsubscribeAllAsync(); ``` ### 历史柱状图 ``` var history = await client.MarketData.GetHistoryAsync( conid: 265598, period: "5d", bar: "1h", outsideRth: false); foreach (var bar in history!.Data!) Console.WriteLine($"{bar.TimeMs}: O={bar.Open} H={bar.High} L={bar.Low} C={bar.Close} V={bar.Volume}"); ``` ### 搜索合约并获取行权价 ``` // 1. Find AAPL var results = await client.Contracts.SearchAsync("AAPL", secType: "STK"); long conid = results![0].Conid; // 2. Get option strikes for a specific expiry month var strikes = await client.Contracts.GetStrikesAsync(conid, month: "SEP25", secType: "OPT"); Console.WriteLine($"Call strikes: {string.Join(", ", strikes!.Call!)}"); ``` ### 投资组合快照 ``` string accountId = "DU1234567"; var positions = await client.Portfolio.GetFirstPositionsAsync(accountId); var allocation = await client.Portfolio.GetAllocationAsync(accountId); var ledger = await client.Portfolio.GetLedgerAsync(accountId); var pnl = await client.Account.GetPnlAsync(); Console.WriteLine($"Positions: {positions?.Length}"); Console.WriteLine($"USD cash: {ledger!["USD"].CashBalance}"); Console.WriteLine($"Day PnL: {pnl?.Upnl?.Values.Sum(x => x.Dpl):F2}"); ``` ### 创建价格警报 ``` var alert = new CreateAlertRequest { OrderType = "P", AlertName = "AAPL above 200", AlertRepeatable = 1, Conditions = [ new AlertCondition { Type = 1, Conid = 265598, Operator = ">=", TriggerValue = "200.00", TimeZone = "America/New_York" } ] }; await client.Alerts.CreateAlertAsync("DU1234567", alert); ``` ### 保活循环 ``` using var cts = new CancellationTokenSource(); _ = Task.Run(async () => { while (!cts.Token.IsCancellationRequested) { await Task.Delay(TimeSpan.FromMinutes(4), cts.Token); await client.Auth.TickleAsync(cts.Token); } }, cts.Token); ``` ## 安全性 ``` flowchart TD A[IBPortalClientOptions] --> B{IgnoreSslErrors = true?} B -- Yes --> E[Bypass TLS validation\nDangerousAcceptAnyServerCertificateValidator] B -- No --> C{Host is loopback?\nlocalhost / 127.x.x.x / ⟨::1⟩} C -- Yes --> E C -- No --> F[Full TLS validation\nstandard certificate chain check] style E fill:#f66,color:#fff style F fill:#6a6,color:#fff ``` 该库的关键安全特性: - **`IgnoreSslErrors` 默认为 `false`** —— 对于非本地主机必须显式启用 - **Localhost 自动绕过** —— `localhost`、`127.x.x.x` 和 `::1` 始终绕过 TLS(IB Gateway 附带自签名证书;这是预期行为) - **在远程主机上设置 `IgnoreSslErrors = true`** 存在中间人 (MITM) 攻击风险 —— 仅在受控的内部网络中使用 - **所有字符串参数在插入查询字符串之前都经过 URL 编码**(`Uri.EscapeDataString`) - **`PingAsync` 仅捕获 `HttpRequestException` / `TaskCanceledException`** —— 意外异常(包括安全错误)会传播 - **机密信息不包含在源代码控制中** —— `appsettings.integration.local.json` 已被 gitignore;仅提交占位符值 ## 测试 单元测试 完全离线 —— 不需要 Gateway。使用 `Moq` 在处理程序级别拦截 HTTP。 ``` dotnet test IB.ClientPortal.Client.UnitTests ``` 71 个测试涵盖: - 每个域客户端的响应反序列化 - 安全场景(`SecurityTests.cs`):SSL 绕过逻辑、特殊字符的 URL 编码、`PingAsync` 异常处理 ### 集成测试 需要运行中的 IB Gateway 和有效会话。首先填充 `appsettings.integration.local.json`(请参阅 [配置](#configuration))。 ``` dotnet test IB.ClientPortal.IntegrationTests ``` 程序集级别的 `GlobalSetup` 会创建单个共享的 `IBPortalClient`,验证身份验证,并在任何测试运行前预热 `/iserver/accounts`。
标签:API 封装, HttpClient, IB Gateway, Interactive Brokers, .NET 8, NuGet 包, REST API, WebAPI 客户端, 外汇交易, 市场数据, 异步编程, 盈透证券, 算法交易, 组合管理, 股票交易, 自动交易系统, 量化交易, 金融科技