BotDeepSeek/AGENTS.md
2026-08-12 01:46:19 -07:00

107 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# AGENTS.md — AI 助手项目上下文
> 本文件供 AI 编码助手(如 OpenCode在新会话中快速恢复项目上下文。
> **修改本项目后请同步更新本文件。**
## 项目概述
Interactive Brokers 股票自动交易机器人Python + ib_insync多策略并行。
**实盘运行**:账户 `U4845070`IB Gateway 端口 4001Gateway 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]` |
| `analytics.py` | 全历史往返交易分析引擎(纯离线,不连 IBFIFO 配对成 `RoundTrip` → 胜率/期望值/盈亏比/profit factor/最大回撤/**保本胜率**,并按策略、标的、入场信号、出场原因、时段归因。`--json` 可供程序消费;`dashboard.py` 依赖它 |
| `dashboard.py` / `dashboard.sh` | 把 `analytics` 结果渲染成自包含 HTML 看板(无 CDN、无外部资源。图表为手写内联 SVG + 原生 JS数据以 JSON 内嵌,筛选在浏览器端重算(`summarize()` 是 `analytics.summarize` 的 JS 镜像,**改动统计口径时两处都要改** |
| `test_offline.py` | 离线单元测试(不连 IB、不下单账本字段、FIFO 配对、统计口径、指标数学。改策略后必跑 `.venv/bin/python -m unittest test_offline` |
| `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` | 主循环60sdisconnectedEvent 只注册一次且有并发/关机防护;重连后 `bar_manager.reset()` + 重新 `on_start` |
## 关键设计决策(不要违反)
1. **持仓归属**IB 只报账户级持仓,归属靠 `bot_state.json`。策略**只卖自己的持仓**。账户中未被追踪的持仓标记为 UNMANAGED永不出卖。
2. **交易所硬止损(主)+ 软止损(兜底)**:买入成交后立即挂 GTC STP 卖单(`stop_loss_pct`outsideRth=Truebot 停机/断线期间仍由交易所执行。`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`$200007-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 起同价 flipNOK 卖→买跨策略、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. **账本记录归因字段2026-08-12 起)**`trades.jsonl` 每条记录附带 `reason`(买入=入场信号名,卖出=出场原因)与 `commission`IB 实际佣金,`orders.trade_commission` 尽力获取,取不到则不写该字段)。出场原因取值:`SignalExit`/`SoftStop`/`HardStop`/`HardStopDuringCancel`/`HardStopDuringTrail`/`HardStopOffline`/`MaxHold`MeanRev 另有 `RSI`/`Bollinger`/`Recovery`。**新增出场分支时必须传 reason**,否则归因图出现 `(none)` 桶。字段缺失即视为未知(不是 0旧账本仍可解析。
12. **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以非终态 OrderStatusfilled=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)。
- **僵尸 STPPreSubmitted 永不变更)+ `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` 已于 2026-08-11 废弃git 仓库已建立,无需再同步备份目录;目录将删除)。
## 环境特殊性
- `.venv` **无 pip/ensurepip**;装包用 `python3 -m pip install --target=.venv/lib/python3.12/site-packages <pkg>`
- pandas 3.0.3:避免使用已移除的 API如 groupby.apply 的 include_groupsVWAP 用 groupby+cumsum 向量化实现
- 系统无 `at`,有一次性任务用 **cron + 执行后自删**`crontab -l | grep -v <pattern> | 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 APIclientId=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 GatewayGUI需登录状态
./restart_bot.sh # 重启 bot杀旧进程 + nohup 启动)
tail -f trading_bot.log # 运行日志
cat bot_state.json # 当前策略持仓归属与成本
./report.sh [YYYY-MM-DD] # 单日成交明细
.venv/bin/python analytics.py # 全历史往返统计(含保本胜率)
./dashboard.sh # 生成并打开 HTML 复盘看板
.venv/bin/python -m unittest test_offline # 离线测试
```
一次性定时任务示例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.47MAStock 在 ADX≥30+慢MA斜率过滤后仍亏在 NOK08-07 买→08-10 卖 ≈-24非参数问题。MAStock 已有持仓仍归其管理退出,不再开新仓。
- **restart_bot.sh kill 加固08-11**:先 SIGTERM10 秒后存活则升级 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.3997MAStock AAPL x6 @304.08。四笔均有 GTC STP 在交易所clientId=1
- 交易参数:每笔 ~$2000 折股、min_profit 1.5%、入场 2 根K线确认、止损 -2.5% STP2026-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 强制平仓。