问答交流

分钟回测中,ETF 份额拆分后卖出,资产价格异常

由bq3m81rk创建,最终由bq3m81rk 被浏览 1 用户

我们在 M.bigtrader.v35 的分钟回测中,发现 ETF 发生份额拆分后,策略层 context.get_position() 读取到的持仓字段疑似没有同步更新。

具体表现是:

1、拆分后的卖出日前,current_qty / cost_price / last_price / market_value 仍然显示拆分前口径;

2、但实际卖出结果又像按拆分后价格成交,导致账户资产异常下降。

3、我们已经用单标的最小复现脚本验证,行情表里的 adjust_factor 是正常跳变的。

请大神帮忙确认:v35 中 ETF 拆分后,策略层可见的持仓对象字段是否存在同步问题,或者是否有官方推荐的正确处理范式。


复现案例可用 159363.SZ,2025-07-18 买入,2025-07-22 卖出前打印 position,再执行卖出。


\

"""BigQuant v35 ETF 拆分后卖出异常最小复现脚本。

用途:
1. 在拆分前买入指定 ETF;
2. 持有跨过拆分生效日;
3. 在拆分后固定时间卖出前,打印策略层看到的持仓字段;
4. 卖出后打印账户现金,便于和预期结果对比。

当前默认案例:
- 159363.SZ
- 2025-07-18 买入
- 2025-07-22 卖出

预期正常现象:
- 拆分后,current_qty 应翻倍;
- cost_price 应减半;
- last_price 应变为拆分后价格;
- market_value 应与拆分前同口径连续。

当前异常现象(历史复现):
- 卖出前 position 里的 qty / cost_price / last_price / market_value 仍是拆分前口径;
- 卖出后账户现金/净值像按拆分后价格成交,导致资产近乎减半。
"""

from bigmodule import M
from bigquant import bigtrader


TARGET_SYMBOL = "159363.SZ"
START_DATE = "2025-07-18"
END_DATE = "2025-07-22"
BUY_TIME = "13:12"
SELL_TIME = "13:10"
BENCHMARK = "510300.SH"
CAPITAL_BASE = 200000


def _time_key(dt_value):
    return dt_value.strftime("%H:%M")


def _position_snapshot(position):
    if position is None:
        return None
    result = {}
    # 这里只保留和“拆分后仓位是否同步更新”直接相关的字段,
    # 方便客服对照看:
    # 1. 数量是否翻倍;
    # 2. 成本是否减半;
    # 3. 最新价是否切到拆分后价格;
    # 4. 市值是否保持连续。
    for field in (
        "current_qty",
        "amount",
        "avail_qty",
        "cost_price",
        "cost_basis",
        "last_price",
        "market_value",
    ):
        value = getattr(position, field, None)
        if value is not None:
            result[field] = value
    return result


def initialize(context):
    context.buy_done = False
    context.sell_done = False
    context.after_sell_logged = False
    # 手续费尽量简化,避免把问题混到交易成本里。
    context.set_commission(
        bigtrader.PerOrder(
            buy_cost=0.0001,
            sell_cost=0.0001,
            min_cost=5,
            tax_ratio=0,
        )
    )


def before_trading_start(context, data):
    # 分钟级回测里,先订阅标的分钟线,确保后续能正常下单和读分钟价格。
    context.subscribe_bar([TARGET_SYMBOL], "1m")


def handle_data(context, data):
    now = data.current_dt
    today = now.strftime("%Y-%m-%d")
    tkey = _time_key(now)

    # 第一步:在拆分前买入并满仓持有。
    # 这里不做任何选股、止损、换仓,只保留最小交易动作。
    if not context.buy_done and today == START_DATE and tkey >= BUY_TIME:
        result = context.order_target_percent(TARGET_SYMBOL, 1.0)
        context.buy_done = True
        print(
            f"PROBE_BUY date={today} time={tkey} symbol={TARGET_SYMBOL} result={result}",
            flush=True,
        )
        return

    position = context.get_position(TARGET_SYMBOL)

    # 第二步:在拆分后的卖出日前,先打印一次策略层看到的持仓对象。
    # 如果引擎处理正确,这里应该已经看到:
    # - current_qty 翻倍
    # - cost_price 减半
    # - last_price 变成拆分后价格
    # 这个日志就是拿来和预期做直接对照的。
    if not context.sell_done and today == END_DATE and tkey >= SELL_TIME and position is not None:
        print(
            "PROBE_BEFORE_SELL "
            f"date={today} time={tkey} symbol={TARGET_SYMBOL} "
            f"cash={getattr(context.portfolio, 'cash', None)} "
            f"available_cash={getattr(context.portfolio, 'available_cash', None)} "
            f"position={_position_snapshot(position)}",
            flush=True,
        )
        result = context.order_target_percent(TARGET_SYMBOL, 0.0)
        context.sell_done = True
        print(
            f"PROBE_SELL date={today} time={tkey} symbol={TARGET_SYMBOL} result={result}",
            flush=True,
        )
        return

    # 第三步:卖出后只打印一次现金结果。
    # 正常情况下,卖出后的现金应与拆分前总资产连续;
    # 如果这里明显减半,而卖出前 position 又还是拆分前口径,
    # 就能说明“策略层持仓字段”和“实际卖出执行结果”不一致。
    if context.sell_done is True and not context.after_sell_logged and today == END_DATE and tkey >= "13:12":
        print(
            "PROBE_AFTER_SELL "
            f"date={today} time={tkey} symbol={TARGET_SYMBOL} "
            f"cash={getattr(context.portfolio, 'cash', None)} "
            f"available_cash={getattr(context.portfolio, 'available_cash', None)}",
            flush=True,
        )
        context.after_sell_logged = True


m5 = M.bigtrader.v35(
    # 只放一个标的,避免其他持仓干扰复现结果。
    data={"instruments": [TARGET_SYMBOL]},
    start_date=START_DATE,
    end_date=END_DATE,
    initialize=initialize,
    before_trading_start=before_trading_start,
    handle_data=handle_data,
    capital_base=CAPITAL_BASE,
    frequency="1m",
    product_type="股票",
    rebalance_period_type="交易日",
    rebalance_period_days="1",
    order_price_field_buy="close",
    order_price_field_sell="close",
    benchmark=BENCHMARK,
    volume_limit=0,
    plot_charts=True,
)

\

{link}