Files
big-qmt/docs/ipo-audit-2026-09-05.md

69 lines
6.1 KiB
Markdown
Raw Normal View History

2026-09-05 23:36:25 +08:00
# IPO 策略审计
日期2026-09-05。范围当前 `py-client/strategy/ipo`,以及直接相关的 SDK、文件锁、调度入口和 `api/qmt_rest_new.py`。仅审计,不修改策略、不调用真实交易接口。
## 结论与分级
本次发现 **P00 项P12 项P23 项**。只列当前问题,不沿用历史报告的已修复项。
- P0需立即处理的全面故障或迫切严重风险。本次未确认此类问题。
- P1会影响申购防重或漏申购建议实盘前优先处理。
- P2特定部署或异常数据条件下影响可靠性安排修复。
## P1-1提交结果不确定时没有防重记录下一次任务可能再次申购
- 位置:`py-client/strategy/ipo/boot.py:47``:51``:59``py-client/libs/lockfile.py:9`
- 证据:先检查文件,再调用 `passorder()`,仅在正常返回后写锁;没有提交前记录、本地订单号或券商订单查询。异常只记录后继续。
- 触发:券商已收到申购,但响应超时或无法解码;或者提交后进程退出、写锁失败。下次 14:00 调度或重启后的任务仍看不到标记。
- 影响:再次发出申购请求;是否拒绝重复申购由柜台决定,不能认为必然重复成交。
- 验证:模拟“已提交但响应超时”,连续执行两次,提交调用为 2 次,锁记录为 0。
- 建议以账户、证券和申购批次生成稳定标识提交前记录待确认结果不确定时先查券商委托不直接重发。SDK 对下单 POST 本身没有自动重试,但不能解决跨任务重发。
## P1-2接口正常返回即永久锁定没有申购结果核对
- 位置:`boot.py:51``:66``api/qmt_rest_new.py:237` 起的 `PassorderHandler`
- 证据:策略不检查返回内容,正常返回就写入 `LOCK`。服务端在底层 `passorder()` 没有抛异常时返回 `status=success`,这不是后续券商成交或有效申购状态。任务不查委托、拒单或可申购额度变化。
- 触发:请求返回正常,但后续发生柜台拒单、数量不符合要求等;或者响应内容未能证明有效申购。
- 影响:标记一直存在,后续任务直接跳过,无法补救当天漏申购。
- 验证:模拟正常响应后第二次执行,提交次数仍为 1即便后续申购失败现有流程也无状态入口解除锁。
- 建议:区分“待确认”与“申购确认成功”;对明确拒绝可恢复尝试,对结果未知继续保留待确认。仅检查 HTTP 成功或服务端 success 字段不足以替代券商对账。
## P2-1锁键没有账户隔离且检查和创建不是原子操作
- 位置:`boot.py:47``libs/lockfile.py:9``:14``main.py``check_single_instance()` 与调度配置。
- 证据:路径仅为 `qmt_data_dir / 股票代码.lock`,不包含账户;`is_file()` 与普通 `write_text()` 分离,也没有互斥。
- 触发:不同账户共用同一数据目录,一个账户的标记使另一个账户跳过;多个项目副本或其他入口同时执行时,也可能都通过检查后提交。
- 影响:跨账户漏申购,或并发重复请求。
- 边界:主入口已有项目路径级单实例保护,调度器设置 `max_instances=1`,因此不把正常单入口运行报告为必然并发;这些保护不能覆盖不同项目路径或独立调用。
- 建议:锁键至少包含账户与证券,明确批次生命周期;如果支持多个进程共享目录,使用原子占位并结合待确认对账。
## P2-2异常 IPO 响应被 SDK 静默当作无候选
- 位置:`py-client/sdk/trade.py:47``:54``api/qmt_rest_new.py:278` 起的 `IpoDataHandler`
- 证据:`ipo_data()` 对非列表返回值直接返回空列表;服务端直接转发底层 IPO 查询结果。策略也不记录候选数量或响应结构异常。
- 触发:接口发生结构变化、返回对象或空值,而不是约定的 `list[dict]`
- 影响:查询异常与正常“今天无新股”无法区分,任务表面正常结束但可能遗漏申购。
- 建议:严格区分合法空列表与错误结构;错误结构记录或抛异常交给任务级边界,避免静默吞掉。
## P2-3候选字段转换不能保证代码及数值有效
- 位置:`boot.py:38``:45`
- 证据:`stock=None` 被转成字符串 `"None"`;代码直接用于文件路径,没有验证是否包含路径分隔符。`float('nan') <= 0` 为假,非有限价格可越过正数检查;`int()` 会截断非整数浮点额度。
- 影响:异常数据产生错误代码请求、非有限价格编码异常、错误数量或非预期锁路径。逐项捕获可隔离异常,但不能让错误数据变正确。
- 建议:对代码做最小格式约束,价格要求有限正数,数量要求正整数而非静默截断。不要把 API 数据直接视为安全路径片段。
- 边界:该风险依赖异常接口数据,不断言当前接口实际返回这些值。
## 待确认行为(不计入问题数量)
- 仅在 10:00、14:00 调度14:00 后启动不补跑当天任务,短暂接口失败也没有同一时段内重试。是否需要启动补跑或有限重试取决于业务要求。
- `trading_time()` 只检查工作日及时间段,不检查交易所节假日;不是完整交易日历。本次不恢复历史上已明确不需要的交易日校验。
- `.BJ` 被主动跳过,仅请求 `STOCK`;视为当前范围选择,不将未参与北交所或债券申购直接判为缺陷。
- `maxPurchaseNum` 被直接用作委托数量,没有结合账户专属可申购额度进一步计算。其究竟是发行上限还是账户可用额度,需要接口实际契约确认,本次不猜测外部业务含义。
## 验证范围
- 两个 IPO 源文件语法检查通过。
- 使用提取的真实任务函数及内存模拟客户端、锁集合,验证响应超时重发和正常响应后跳过;四次模拟均执行客户端退出方法。
- 列表逐行解析、候选级异常隔离、任务级异常捕获、客户端上下文释放均已检查。
- 未执行真实申购、撤单、外部网络请求或交易所规则核验;未创建测试锁文件。模拟验证不代表端到端实盘验收。