#3791·vnpy

[Bug] PositionHolding在持仓推送后未重算已有活动委托的冻结量

Author: ai-yangCreated Jul 31, 2026Updated Sep 8, 2026

环境

  • 操作系统: Ubuntu 22.04.4 LTS x86_64
  • Python版本: Python 3.10.12、Python 3.13.14
  • VeighNa版本: dev branch 4240fee6925313d84649b6f5b86f4f3a7b679ec4(提交日期2026-07-28,vnpy.__version__ == "4.4.0"

Issue类型

Bug

预期程序行为

PositionHolding已经缓存活动平仓委托时,后续收到PositionData持仓快照后,应根据最新持仓和已有的active_orders重新计算冻结量。

以下场景中已有一笔卖出平今5手的活动委托,随后收到多头今仓5手的持仓快照,因此:

  • long_td_frozenlong_pos_frozen应为5;
  • 可用多头持仓应为0;
  • 再次转换卖出平仓5手的请求时,应返回空列表,避免重复平仓。

实际程序行为

PositionHolding.update_position()只更新long_pos/long_yd/long_td或对应的空头字段,没有重新调用calculate_frozen()

如果活动委托先到达,此时持仓仍为0,update_order()计算出的冻结量会被限制为0。之后持仓快照到达时冻结量仍保持为0,转换器会错误地再次生成5手平今委托。

在Python 3.10和3.13下分别连续运行3次,结果均为:

{'active_order_count': 1, 'long_pos': 5, 'long_td': 5, 'long_pos_frozen': 0, 'converted': [('CLOSETODAY', 5)]}
AssertionError: 活动平今委托的剩余数量应在持仓快照到达后重新冻结

该状态如果没有后续的委托更新,会一直保留,并可能造成重复平仓请求。

重现步骤

以下脚本不连接交易接口或其他外部服务:

python
from vnpy.trader.constant import Direction, Exchange, Offset, OrderType, Product, Status
from vnpy.trader.converter import PositionHolding
from vnpy.trader.object import ContractData, OrderData, OrderRequest, PositionData


contract = ContractData(
    gateway_name="CTP", symbol="rb", exchange=Exchange.SHFE,
    name="rb", product=Product.FUTURES, size=10, pricetick=1,
)
holding = PositionHolding(contract)

holding.update_order(OrderData(
    gateway_name="CTP", symbol="rb", exchange=Exchange.SHFE,
    orderid="1", direction=Direction.SHORT, offset=Offset.CLOSETODAY,
    type=OrderType.LIMIT, volume=5, traded=0, status=Status.NOTTRADED,
))
holding.update_position(PositionData(
    gateway_name="CTP", symbol="rb", exchange=Exchange.SHFE,
    direction=Direction.LONG, volume=5, yd_volume=0,
))

request = OrderRequest(
    symbol="rb", exchange=Exchange.SHFE, direction=Direction.SHORT,
    type=OrderType.LIMIT, volume=5, offset=Offset.CLOSE,
)
converted = holding.convert_order_request_shfe(request)

print({
    "active_order_count": len(holding.active_orders),
    "long_pos": holding.long_pos,
    "long_td": holding.long_td,
    "long_pos_frozen": holding.long_pos_frozen,
    "converted": [(item.offset.name, item.volume) for item in converted],
})

assert holding.long_pos_frozen == 5, "活动平今委托的剩余数量应在持仓快照到达后重新冻结"
assert converted == []

从仓库根目录连续运行3次,均可观察到long_pos_frozen == 0,且转换器生成新的CLOSETODAY 5请求。