AI4Finance-Foundation / AI4Finance-Foundation/FinRL

Benchmark comparison: agent trades dividend-adjusted prices while the DJIA baseline is price-return only

オープン
#1,434 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る
主要言語
Jupyter Notebook
スター
16.3k
フォーク
3.5k
PR マージ指標
30日以内にマージされた PR はありません

説明

## 1. Summary

In the shipped tutorial, the agent's prices are dividend-adjusted (total return) while the
DJIA baseline is `^DJI`, a price-return index. The two sides of the comparison therefore use
different return definitions, and the reported gap includes the DJIA dividend stream
regardless of what the agent does.

Measured over 2018-01-03..2025-12-31, the convention difference alone is worth **+1.92% per
year (t(NW6) = 9.99, +16.4% cumulative over 8 years)**, in the direction that flatters the
agent. This does not overturn the reported results — FinRL agents are reported well above the
DJIA — but it is a fixed offset sitting inside every such comparison, and it is not a
modelling choice, since no reading of "excess return" pairs a total-return portfolio with a
price-return index.

## 2. Evidence in code

Repository commit at time of writing: `master`.

**The agent's prices are dividend-adjusted.** `YahooDownloader.fetch_data` defaults to
`auto_adjust=False` (`finrl/meta/preprocessor/yahoodownloader.py:36`), which routes through
`_adjust_prices` (`:86`, defined at `:104`):

```python
data_df["adj"] = data_df["adjcp"] / data_df["close"]
for col in ["open", "high", "low", "close"]:
data_df[col] *= data_df["adj"]
```

So `close` becomes Adj Close, which includes distributions.
`examples/FinRL_StockTrading_2026_1_data.py:47` builds `train_data.csv` / `trade_data.csv`
through exactly this path, and `env_stocktrading.py` prices positions off `self.data.close`.
The account value is therefore a total-return series.

**The baseline is price return.** The tutorial does not go through `plot.py`; it constructs
the baseline itself at `examples/FinRL_StockTrading_2026_3_Backtest.py:155`:

```python
df_dji = yf.download("^DJI", start=TRADE_START_DATE, end=TRADE_END_DATE)
df_dji = df_dji[["Close"]].reset_index()
```

`^DJI` is an index, so it has no Adj Close distinct from Close and no adjustment applies under
either `auto_adjust` setting. The same holds on the library path: `plot.py:54` defaults
`baseline_ticker="^DJI"`, and `get_baseline` (`:76`) fetches it through the same downloader,
where `_adjust_prices` is a no-op because `adj == 1`.

The asymmetry is thus present on two independent paths, including the one the README
instructs users to run.

## 3. Measurement

To isolate the convention from any agent behaviour, no agent is involved. `DIA` is an ETF
tracking the DJIA; its Yahoo adjusted close includes distributions. `^DJI` is the same index,
price return only. Same constituents, same weights, same rebalance schedule — the only
difference between the two series is whether distributions are counted.

Window 2018-01-03..2025-12-31, 2010 trading days. Statistics are annualized; `t(NW6)` is a
Newey-West t-statistic on the mean daily difference with 6 lags.

**Control** — if this is not near zero, nothing below is trustworthy:

| Comparison | Annualized | t(NW6) |
| --- | --- | --- |
| DIA price return − `^DJI` | +0.00% | +0.01 |

**The convention difference:**

| Comparison | Annualized | t(NW6) |
| --- | --- | --- |
| DIA total return − DIA price return | +1.92% | +9.99 |
| DIA total return − `^DJI` (as the tutorial pairs them) | +1.92% | +13.82 |

**Levels:**

| Series | Annualized | Vol |
| --- | --- | --- |
| DIA total return | +12.12% | 19.01% |
| DIA price return | +10.20% | 19.04% |
| `^DJI` (baseline used) | +10.20% | 19.08% |

Compounded over the 8-year window the gap is worth +16.4% of cumulative return.

**A discarded first attempt, for transparency.** I first measured this by reconstructing a
price-weighted portfolio from the current DJIA membership list and comparing it against
`^DJI`. That control came out at +1.40%/yr (t = 2.12) rather than near zero, because a
current-membership list over an 8-year window carries survivorship bias. I discarded that
design rather than report from it; the two-ticker construction above needs no membership
list, no reconstruction, and no weighting assumption.

## 4. What this does and does not imply

- It does not imply the reported agent results are wrong in direction. Reported agents sit
well above the DJIA; 1.92%/yr is a fraction of that gap.
- It does not imply Adj Close is the wrong input for the agent. #100 raised a separate
concern about Adj Close and was closed with the position that it is reasonable for
backtesting. I am not disputing that. The point here is only that if one side of the
comparison uses total return, the other side should too.
- It does mean the reported excess is biased upward by a fixed amount that does not depend on
the policy, so cross-run and cross-paper comparisons inherit it.

## 5. Suggested fix

The minimal change is to make the baseline a total-return series. In the tutorial
(`FinRL_StockTrading_2026_3_Backtest.py:155`) and as the `plot.py:54` default:

```python
df_dji = yf.download("DIA", start=TRADE_START_DATE, end=TRADE_END_DATE, auto_adjust=True)
```

Caveat: `DIA` carries an expense ratio (~0.16%/yr), so it slightly understates a pure DJIA
total-return index. If maintainers prefer an index series over an ETF proxy, that is a better
choice where one is available; `DIA` is offered here because it needs no new data source.

If changing the default is unwanted, documenting the convention at both sites would at least
let readers interpret the number correctly.

I am happy to open a PR for whichever direction maintainers prefer.

## 6. Prior discussion

I searched the issue tracker including closed issues for `dividend`, `adjusted close`,
`adj close`, `benchmark`, and `DJI baseline`. The closest prior thread is #100, which
concerned look-ahead in Adj Close rather than benchmark-side consistency; the asymmetry
described here was not raised there. #746 and #1303 report that baseline and agent numbers do
not reproduce paper values, but the discrepancy in #746 is roughly 0.12 percentage points and
is between two DJIA computations, so it is not explained by this finding and I make no claim
about it. Apologies if I have missed an existing issue.

For context on method: I checked this after finding a related benchmark-comparison question in
another framework (microsoft/qlib#2315), using the same control design of measuring the
convention with no model involved.

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

評価

この issue はまだ評価されていません。

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。