# AGENTS.md — AI 助手项目上下文 > 本文件供 AI 编码助手(如 OpenCode)在新会话中快速恢复项目上下文。 > **修改本项目后请同步更新本文件。** ## 项目概述 Interactive Brokers 股票自动交易机器人(Python + ib_insync),多策略并行。 **实盘运行**:账户 `U4845070`,IB Gateway 端口 4001(Gateway Live)。 另有账户内独立持仓 `HF x300` —— 永远不属于 bot 管理,禁止触碰。 交易标的(1分钟K线,每策略每标的最多1笔):**AMZN、AAPL、NVDA、NOK**(2026-08-03 起加 NVDA、NOK;此前为11只科技股的组合已停用)。 每标的持仓价值上限 `max_symbol_value_usd`=$2300(三策略合计,`tracker.symbol_value()` 计算)。 ## 文件结构与职责 | 文件 | 职责 | |------|------| | `config.py` | dataclass 配置;自动加载 `.env`(python-dotenv);`state_file` 为基于 `__file__` 的绝对路径 | | `connection.py` | IB 连接单例 `ib_conn`(connect/reconnect/ensure_connected) | | `bars.py` | `BarManager`:每个合约一条 `keepUpToDate=True` 实时K线订阅,多策略共享;`to_completed_df` 丢弃未收盘bar;`is_market_active` 通过最新bar时效判断开闭市 | | `state.py` | `PositionTracker`:持仓归属(哪个策略拥有哪笔仓位),持久化到 `bot_state.json`;启动/重连时 `reconcile()` 对账;每笔买卖追加写入 `trades.jsonl` 流水账本(首次运行以当前持仓为 seed) | | `daily_report.py` / `report.sh` | 当日成交明细+盈亏报告(FIFO,数据源 `trades.jsonl`);用法 `./report.sh [YYYY-MM-DD]` | | `orders.py` | `execute_market_order`:下单+等待成交(30s超时撤单)+防重复单(`has_open_order`,按 orderRef 匹配) | | `strategies/` | `ma_cross`(SMA20/50+ADX)、`short_term`(EMA5/10+VWAP+max_hold)、`mean_reversion`(RSI/布林/急跌抄底)、`forex`(默认关闭) | | `main.py` | 主循环60s;disconnectedEvent 只注册一次且有并发/关机防护;重连后 `bar_manager.reset()` + 重新 `on_start` | ## 关键设计决策(不要违反) 1. **持仓归属**:IB 只报账户级持仓,归属靠 `bot_state.json`。策略**只卖自己的持仓**。账户中未被追踪的持仓标记为 UNMANAGED,永不出卖。 2. **交易所硬止损(主)+ 软止损(兜底)**:买入成交后立即挂 GTC STP 卖单(`stop_loss_pct`,outsideRth=True),bot 停机/断线期间仍由交易所执行。`base.py` 止损单机制:每周期 `_sync_stop_orders()`(成交→record_sell;终态订单按 `_trade_uid`(permId/orderId) **只消费一次**;只认领未完成订单;owned 但无单→补挂);策略卖出前 `_cancel_stop()` 并处理撤单竞态中的成交;软止损仅在 `_has_active_stop()==False` 时兜底。外汇仍用软止损(市场连续,且策略默认关闭)。 3. **卖出不阻塞**:死叉类退出若盈利不足 `min_profit_pct`(1.5%),**每轮重检**(不是等下次交叉事件),达标即卖。 4. **按金额定股数**:每笔 `trade_value_usd`($2000,07-27 从 $1000 提高)按信号价折算(`base._order_quantity`,floor、最少1股);佣金占比约 0.05%+0.1%往返。 5. **全局持仓上限**:`config.max_positions`(12 批,≈$24k 敞口,07-28 从 15 调低)。策略买入前检查 `tracker.total_positions()`,达上限则跳过买入(日志 SKIPPED);已有持仓正常管理退出,不强制平仓。 6. **每日亏损熔断**:`config.max_daily_loss`($150)。`PositionTracker` 按 record_sell 实时累计当日已实现盈亏(持久化 `day_pnl.json`,跨重启保留、本地午夜重置),触限后当日禁止开新仓(`base._can_open_position` 统一闸门,返回原因)。 7. **止损/失败冷却**:同一策略同一标的被止损(硬止损成交或软止损卖出)后 `stop_cooldown_minutes`(30分钟)内禁止重买;下单被拒/超时也进冷却(防每周期重试刷屏)。 8. **全局卖出冷却(跨策略防 flip-flop)**:任何策略卖出某标的后,所有策略在 `sell_cooldown_minutes`(30分钟)内禁止买入该标的;超过冷却但仍在 `sell_improvement_window_minutes`(60分钟)内,重买价必须 ≥ `sell_improvement_pct`(0.5%)低于最近卖价。登记点统一在 `tracker.record_sell` / `_record_external_sell`(覆盖策略卖出、硬止损成交、撤单竞态成交、断线回填),持久化到 `recent_sells.json`(跨重启保留,惰性清理)。2026-08-04 修复当日 3 起同价 flip:NOK 卖→买(跨策略)、NVDA 卖→买(同策略 4 分钟原价买回)、AAPL 卖→买(跨策略)。 8. **MeanRev 趋势过滤**:仅在 `close > SMA(trend_ma_period=50)` 时允许抄底,避免下跌趋势中接飞刀。 9. **信号只用已收盘K线**:最后一根 forming bar 必须丢弃(时间戳比较法)。 10. **休市禁交易**:最新bar年龄 > `bar_seconds*5` 视为休市,跳过全部信号。收盘后约5分钟内 bar 仍"新鲜",此窗口的市价单会隔夜排队——收市停机需提前(参考 15:59 EOD 停止的做法)。 11. **K线流量**:禁止每轮全量拉历史数据(旧版 5 小时 845MB);用 keepUpToDate 订阅(约 3.5MB/5小时)。`formatDate=2`(epoch,时区无歧义)。 ## ⚠️ 已修复的 bug(勿重新引入) - **ADX**:`down_move = -low.diff()`(Wilder 定义),**不是** `low.diff().abs()`。abs 版本会把上涨中的低点抬升误记为空头 DM,导致趋势方向完全颠倒。 - **RSI**:`rs = avg_gain / avg_loss` 直接除,让 `inf` 自然传播(纯涨→RSI 100)。不要 `.replace(0, nan)`,否则纯涨时 RSI 变 NaN。 - **state_file 必须绝对路径**:cron/nohup 启动时 CWD 是 `$HOME`,相对路径会把状态文件写错位置。 - **从 shell 工具启动 bot 必须 `setsid`**:`setsid nohup .venv/bin/python main.py >> trading_bot.log 2>&1 < /dev/null &`。否则终端会话结束/工具超时杀进程组时 bot 会被带走。 - **BarManager.subscribe 竞态**:先存 in-flight Task 再 await(重连任务的 `on_start` 与主循环 `on_bar` 会并发订阅同一合约,旧代码 `await` 后才存导致重复订阅+订阅泄漏)。主循环断线期间直接跳过周期。 - **断线窗口的止损成交必须回填账本**:STP 在 bot 断线时成交,bot 收不到事件;`reconcile()` 清理/裁剪仓位时必须调用 `_record_external_sell` 补记 `trades.jsonl`(价格取 `reqCompletedOrders` 实际成交价,取不到则按止损价 -2% 估记并标 `est:true`)。否则报告出现幽灵持仓、漏记亏损(07-30 修复,含一次性历史修复)。 - 重连事件重复注册、关机后误触发重连:已在 main.py 修复,注意保持 `_running` 与 `_reconnect_task` 防护逻辑。 - **`reqCompletedOrdersAsync` 必须传 `apiOnly` 参数**:ib_insync 0.9.86 起签名必填。bot 回填断线 STP 成交用 `apiOnly=True`(只取 API 订单,天然排除 TWS 手工单)。不传会抛 TypeError,断线回填静默失效(07-31 修复)。 - **`reqCompletedOrdersAsync` 会把已完成订单注入 `wrapper.trades` 造成"幻影订单"**:ib_insync 0.9.86 的 `wrapper.completedOrder`(wrapper.py:402)以非终态 OrderStatus(filled=0、status 取 orderState.status)把全部已完成订单塞进 `ib.trades()`/`ib.openTrades()`——同 ref 的僵尸单(如 08-03 遗留 NOK STP)在重启后以 `PreSubmitted/done=False` 现形 → `has_open_order` 永远返回 True → 该 key 的止损**永不补挂**(持仓裸奔);done 幻影还会触发每轮 "ended with status=Filled - re-placing now" 噪音。防御(08-04 修复):`main._completed_stp_fills` 收集完成交价后按 `permId` 把幻影从 `ib.wrapper.trades` 弹出;`orders.has_open_order` 另加 `remaining > 0` 硬条件(幻影 remaining 恒为 0,真实在挂单 >0)。 - **外部成交价 0.0 必须按"未知"处理**:`reqCompletedOrders` 对跨会话(上次 bot 会话)的已完成订单可能返回 `avgFillPrice=0.0`。`main.py` 的 `_completed_stp_fills` 要跳过 `fill_price <= 0` 的订单;`state.py` 的 `_record_external_sell` 用 `estimated = not price`(None 或 0.0 都视为取不到)→ 按止损价 -2% 估记并标 `est:true`。否则会把 0.0 当真价写账本,亏损虚算(07-31 修复:迁移目录时踩中,造成 day_pnl 虚增 -1668.74)。 - **僵尸 STP(PreSubmitted 永不变更)+ `has_open_order` 造成"假卡死"**:被风控拒掉/IB 端失同步的 STP 会以 PreSubmitted 卡在 `ib.openTrades()` 里(isDone()=False),`has_open_order` 永远返回 True → `_place_stop` 静默 skip,且 `_raise_stop_if_needed` 只在 target>current 时撤单重挂,价格不符的僵尸单永不处理 → bot 看起来"卡死"(无日志,持仓无有效 STP)。症状:`reqAllOpenOrdersAsync`(跨 clientId)可见这些单,`cancelOrder` 报 Error 10147 取不掉。恢复:`reqGlobalCancel()` 清掉全部僵尸单后重启。防御(08-03 修复):`_raise_stop_if_needed` 改为 **auxPrice 与目标价不匹配(容差 <0.01)就撤单重挂**,不只处理上移。另:`has_open_order` 只看 `openTrades()`(本 clientId 自己的单),`reqOpenOrdersAsync` 返回本 clientId 的订单,诊断全账户订单要用 `reqAllOpenOrdersAsync`。 - **Cancelled STP 必须当轮重挂**:`_sync_stop_orders` 消费终态订单后若 owned 且未成交,**同一周期**就 `_place_stop`,不要拖到下轮(旧代码 `continue` 后依赖下一轮 by_ref 找不到已 consumed 的单才重挂,重启后首轮只挂 1 条就停)。08-03 修复。 - **正式目录是 `~/Desktop/DeepSeek/BotDeepSeek`**:bot 自此从该目录启动(`restart_bot.sh` 自定位)。`~/Desktop/kimi/ib-trading-bot-kimi` 仅作备份保留,勿再从那里启动;两目录代码保持一致。 ## 环境特殊性 - `.venv` **无 pip/ensurepip**;装包用 `python3 -m pip install --target=.venv/lib/python3.12/site-packages ` - pandas 3.0.3:避免使用已移除的 API(如 groupby.apply 的 include_groups);VWAP 用 groupby+cumsum 向量化实现 - 系统无 `at`,有一次性任务用 **cron + 执行后自删**(`crontab -l | grep -v | crontab -`),参考 `close_legacy_positions.sh` / `stop_bot_eod.sh` - 系统时区 EDT = 美股时间;cron 按本地时间 - IB Gateway 自带 JRE 17(`~/.local/share/i4j_jres/`);`~/jdk`(Temurin 21)是备用,`start_gateway.sh` 引用它 - Gateway API:clientId=1 给 bot;临时只读检查用 clientId=77,查完即断开 ## 安全红线 - **这是实盘账户**:任何测试不得真实下单。验证用离线测试(纯函数:指标/状态/K线工具),参考做法:构造合成数据测 `_calc_rsi/_calc_adx/_calc_vwap`、`PositionTracker`、`to_completed_df` - 不要为了"测试"运行 `main.py`——它会下真实订单 - 修改策略逻辑后:先 `py_compile`,再跑离线测试,最后才考虑重启 bot ## 日常操作 ```bash ./start_gateway.sh # 启动 IB Gateway(GUI,需登录状态) ./restart_bot.sh # 重启 bot(杀旧进程 + nohup 启动) tail -f trading_bot.log # 运行日志 cat bot_state.json # 当前策略持仓归属与成本 ``` 一次性定时任务示例(cron):`close_legacy_positions.sh`(平仓+自动启动bot)、`stop_bot_eod.sh`(15:59 收市前停机)。 ## 当前状态(2026-08-11 盘后) - **MAStock 策略已停用(08-11)**:`StockStrategyConfig.enabled=False`,仅跑 ShortTerm + MeanRev。依据:08-03 起已实现 ShortTerm +101.66 / MeanRev -19.98 / MAStock -65.47;MAStock 在 ADX≥30+慢MA斜率过滤后仍亏在 NOK(08-07 买→08-10 卖 ≈-24),非参数问题。MAStock 已有持仓仍归其管理退出,不再开新仓。 - **restart_bot.sh kill 加固(08-11)**:先 SIGTERM,10 秒后存活则升级 SIGKILL(旧 `pkill` 对 setsid 启动/重连循环中的进程无效,08-11 实测 TERM 不响应)。 - **已恢复正常交易**(`sell_only=False`),标的 AMZN/AAPL/NVDA/NOK,每标的持仓价值上限 $2300 - 当前持仓(08-03 盘中):ShortTerm AMZN x7 @285.86、NVDA x9 @208.17、NOK x212 @9.3997;MAStock AAPL x6 @304.08。四笔均有 GTC STP 在交易所(clientId=1) - 交易参数:每笔 ~$2000 折股、min_profit 1.5%、入场 2 根K线确认、止损 -2.5% STP(2026-08-03 从 -2% 放宽,依据 256 笔成交统计:61 笔亏损平仓中 82% 已控制在 -3% 内,2% 执行良好;放宽仅为减少 whipsaw,每笔风险 +$10 仍在 $150 日熔断内)、每日熔断 $150、全局持仓 12 批 - **移动止损方案A**(08-03 实现):盈利 ≥ min_profit(1.5%) 后,STP 从"入场价-2.5%"上移到"持仓期间最高收盘价-2.0%",且永不低于固定止损。`use_trailing_stop/trailing_stop_pct` 在 config 三股票策略里 - **持仓不设上限(08-05 撤销 5 天强制卖出)**:`ShortTermConfig.max_hold_days=0`(0=不限),short_term.py 仅在 `>0` 时触发 max-hold 卖出。持仓由 EMA 死叉+min_profit、硬止损、移动止损自然退出,可长期持有趋势单。 - **全局卖出冷却(08-04 实现,方案C)**:`sell_cooldown_minutes=30`(任何卖出后所有策略禁买)+ `sell_improvement_pct=0.5%`(冷却后**120分钟内**重买须比最近卖价低0.5%,08-06 由60→120)+ 登记在 `recent_sells.json` - **硬止损成交当日禁买(08-06)**:`_mark_hard_stop_cooldown` 使硬止损成交后该标的冷却到**次日零时**(`_next_day_start`),防隔夜跳空止损后当天同价回补;软止损/订单失败冷却仍 30 分钟。 - **NOK 减半仓位(08-06)**:`config.symbol_trade_value_usd={"NOK": 1000.0}`,base `_trade_value_usd` 供各策略 `_order_quantity/_can_open_position` 使用,其余标的一律默认 2000。 - **MAStock 入场过滤收紧(08-06)**:ADX 门槛 25→30(`adx_min`),且新增 `require_slow_ma_slope=True`——仅当慢 MA 自身在确认窗口内上升才允许买入(防下跌趋势中金叉接刀,08-05 NOK 案例)。 - **过夜敞口观察中(2026-07-31 起)**:07-31 早盘 AAPL x5 隔夜跳空 -8.9% 止损成交(STP @265.52→实际 304.145,亏损 $148≈止损预期的4倍)。决策暂不改为 EOD 平仓,先积累 AMZN/AAPL 新 regime 样本(2-4周)统计跳空频率/实际过夜损耗,再决定是否加 EOD 强制平仓。