microsoft/qlib · error · ValueError

Both trade_end_time and bench_value is None, benchmark is no

Error message

Both trade_end_time and bench_value is None, benchmark is not usable.

What it means

In PortfolioMetric.fill, the benchmark value for the step is either passed in (bench_value) or sampled from self.bench between trade_start_time and trade_end_time. If both are None there is no way to compute the benchmark leg, so ValueError('Both trade_end_time and bench_value is None, benchmark is not usable.') is raised before any data is recorded.

Source

Thrown at qlib/backtest/report.py:185

        # 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
        self.benches[trade_start_time] = bench_value
        # update pm
        self.latest_pm_time = trade_start_time
        # finish pm update in each step

    def generate_portfolio_metrics_dataframe(self) -> pd.DataFrame:

View on GitHub (pinned to 79633dd950)

Solutions

  1. Pass trade_end_time (even equal to trade_start_time) whenever a benchmark is configured
  2. Or pass bench_value explicitly (e.g. 0.0) for steps where no benchmark sampling applies
  3. If you do not want benchmark comparison, construct the metric without benchmark_config so the bench path is skipped

Example fix

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

# after
pm.fill(trade_start_time=t, trade_end_time=t, account_value=v, cash=c, ..., bench_value=0.0)
Defensive patterns

Strategy: validation

Validate before calling

if trade_end_time is None and bench_value is None:
    bench_value = 0.0  # or pass trade_end_time=trade_start_time
    # choose based on whether benchmark comparison matters for this step

Try / catch

try:
    pm.fill(..., trade_end_time=trade_end_time, bench_value=bench_value)
except ValueError as e:
    if "benchmark is not usable" in str(e):
        pm.fill(..., bench_value=0.0)  # retry without benchmark sampling

Prevention

When it happens

Trigger: Calling fill without trade_end_time and without bench_value while a benchmark series is configured; or with trade_end_time=None and bench=None (no benchmark was given at init, so _sample_benchmark returns None and the passed bench_value is None).

Common situations: Single-timestamp fills (intraday steps with no end time) that still carry a benchmark config; benchmark_config set to None so init produced self.bench = None, while fill still expects a bench value.

Related errors


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