69 lines
6.1 KiB
Markdown
69 lines
6.1 KiB
Markdown
# IPO 策略审计
|
||
|
||
日期:2026-09-05。范围:当前 `py-client/strategy/ipo`,以及直接相关的 SDK、文件锁、调度入口和 `api/qmt_rest_new.py`。仅审计,不修改策略、不调用真实交易接口。
|
||
|
||
## 结论与分级
|
||
|
||
本次发现 **P0:0 项,P1:2 项,P2:3 项**。只列当前问题,不沿用历史报告的已修复项。
|
||
|
||
- 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 源文件语法检查通过。
|
||
- 使用提取的真实任务函数及内存模拟客户端、锁集合,验证响应超时重发和正常响应后跳过;四次模拟均执行客户端退出方法。
|
||
- 列表逐行解析、候选级异常隔离、任务级异常捕获、客户端上下文释放均已检查。
|
||
- 未执行真实申购、撤单、外部网络请求或交易所规则核验;未创建测试锁文件。模拟验证不代表端到端实盘验收。
|