microsoft/qlib · error · ValueError

None in [trade_start_time, account_value, cash, return_rate,

Error message

None in [trade_start_time, account_value, cash, return_rate, total_turnover, turnover_rate, total_cost, cost_rate, stock_value]

What it means

PortfolioMetric.fill (the per-step record API) requires complete accounting inputs; if any of trade_start_time, account_value, cash, return_rate, total_turnover, turnover_rate, total_cost, cost_rate, stock_value is None it raises ValueError listing them. The method writes one row per trade step into the accounts/returns/turnover/cost dicts, and None would poison all derived report metrics.

Source

Thrown at qlib/backtest/report.py:179

        turnover_rate: float | None = None,
        total_cost: float | None = None,
        cost_rate: float | None = None,
        stock_value: float | None = None,
        bench_value: float | None = None,
    ) -> None:
        # check data
        if None in [
            trade_start_time,
            account_value,
            cash,
            return_rate,
            total_turnover,
            turnover_rate,
            total_cost,
            cost_rate,
            stock_value,
        ]:
            raise ValueError(
                "None in [trade_start_time, account_value, cash, return_rate, total_turnover, turnover_rate, "
                "total_cost, cost_rate, stock_value]",
            )

        if trade_end_time is None and bench_value is None:
            raise ValueError("Both trade_end_time and bench_value is None, benchmark is not usable.")
        elif bench_value is None:
            bench_value = self._sample_benchmark(self.bench, trade_start_time, trade_end_time)

        # update pm data
        self.accounts[trade_start_time] = account_value
        self.returns[trade_start_time] = return_rate
        self.total_turnovers[trade_start_time] = total_turnover
        self.turnovers[trade_start_time] = turnover_rate
        self.total_costs[trade_start_time] = total_cost
        self.costs[trade_start_time] = cost_rate
        self.values[trade_start_time] = stock_value
        self.cashes[trade_start_time] = cash

View on GitHub (pinned to 79633dd950)

Solutions

  1. Supply every required field: default missing numeric fields to 0.0 and a valid trade_start_time
  2. Trace which argument is None: print/log the tuple before calling fill
  3. If a step genuinely had no trades, still pass zeros rather than None so reports stay consistent

Example fix

# before
pm.fill(trade_start_time=t, account_value=v, cash=c)  # rest default to None

# after
pm.fill(trade_start_time=t, account_value=v, cash=c, return_rate=r or 0.0,
        total_turnover=tov or 0.0, turnover_rate=tr or 0.0,
        total_cost=tc or 0.0, cost_rate=cr or 0.0, stock_value=sv or 0.0)
Defensive patterns

Strategy: validation

Validate before calling

required = dict(trade_start_time=trade_start_time, account_value=account_value, cash=cash,
    return_rate=return_rate, total_turnover=total_turnover, turnover_rate=turnover_rate,
    total_cost=total_cost, cost_rate=cost_rate, stock_value=stock_value)
none_fields = [k for k, v in required.items() if v is None]
assert not none_fields, f"fill() missing: {none_fields}"

Try / catch

try:
    pm.fill(...)
except ValueError as e:
    raise RuntimeError(f"incomplete metrics for step: {e}") from e

Prevention

When it happens

Trigger: Calling pm.fill(...) with a forgotten keyword argument (defaults None) or a computed metric that evaluated to None — most often bench_value-related returns or cost fields left unset by custom executors; also positional-argument mix-ups.

Common situations: Custom executors/nested flows calling fill with only a few fields; porting old code to a newer qlib signature that added stock_value; account fields returned as None when the exchange produced no deals.

Related errors


AI-assisted analysis of microsoft/qlib@79633dd950 (2026-08-15). Data as JSON: /api/errors/1ab68d0cd27eed93. Report an issue: GitHub.