rl-pricing 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. rl_pricing-0.5.0/LICENSE +21 -0
  2. rl_pricing-0.5.0/MANIFEST.in +6 -0
  3. rl_pricing-0.5.0/PKG-INFO +86 -0
  4. rl_pricing-0.5.0/README.md +344 -0
  5. rl_pricing-0.5.0/main.py +6 -0
  6. rl_pricing-0.5.0/pyproject.toml +4 -0
  7. rl_pricing-0.5.0/pytest.ini +3 -0
  8. rl_pricing-0.5.0/requirements.in +15 -0
  9. rl_pricing-0.5.0/requirements.txt +14 -0
  10. rl_pricing-0.5.0/rl/__init__.py +0 -0
  11. rl_pricing-0.5.0/rl/autotrain/__init__.py +0 -0
  12. rl_pricing-0.5.0/rl/autotrain/r1.py +124 -0
  13. rl_pricing-0.5.0/rl/autotrain/training.py +933 -0
  14. rl_pricing-0.5.0/rl/bench/__init__.py +5 -0
  15. rl_pricing-0.5.0/rl/bench/cli.py +272 -0
  16. rl_pricing-0.5.0/rl/bench/data.py +383 -0
  17. rl_pricing-0.5.0/rl/bench/engine.py +173 -0
  18. rl_pricing-0.5.0/rl/bench/features.py +114 -0
  19. rl_pricing-0.5.0/rl/bench/figures.py +104 -0
  20. rl_pricing-0.5.0/rl/bench/gym_env.py +125 -0
  21. rl_pricing-0.5.0/rl/bench/leaderboard.py +424 -0
  22. rl_pricing-0.5.0/rl/bench/ledger.py +607 -0
  23. rl_pricing-0.5.0/rl/bench/market.py +178 -0
  24. rl_pricing-0.5.0/rl/bench/monthly.py +387 -0
  25. rl_pricing-0.5.0/rl/bench/policies/__init__.py +1 -0
  26. rl_pricing-0.5.0/rl/bench/policies/baselines.py +290 -0
  27. rl_pricing-0.5.0/rl/bench/policies/forecast.py +212 -0
  28. rl_pricing-0.5.0/rl/bench/policies/sb3.py +337 -0
  29. rl_pricing-0.5.0/rl/bench/policies/tradingagents.py +364 -0
  30. rl_pricing-0.5.0/rl/bench/policy.py +53 -0
  31. rl_pricing-0.5.0/rl/bench/runner.py +408 -0
  32. rl_pricing-0.5.0/rl/bench/scoring.py +279 -0
  33. rl_pricing-0.5.0/rl/bench/submission.py +124 -0
  34. rl_pricing-0.5.0/rl/bench/sweep.py +561 -0
  35. rl_pricing-0.5.0/rl/bench/tasks.py +206 -0
  36. rl_pricing-0.5.0/rl/bench/verdicts.py +265 -0
  37. rl_pricing-0.5.0/rl/cli.py +299 -0
  38. rl_pricing-0.5.0/rl/config/__init__.py +0 -0
  39. rl_pricing-0.5.0/rl/config/config.py +91 -0
  40. rl_pricing-0.5.0/rl/env/__init__.py +0 -0
  41. rl_pricing-0.5.0/rl/env/env_portfolio.py +261 -0
  42. rl_pricing-0.5.0/rl/env/env_stocktrading.py +406 -0
  43. rl_pricing-0.5.0/rl/forecast/__init__.py +0 -0
  44. rl_pricing-0.5.0/rl/forecast/lstm.py +238 -0
  45. rl_pricing-0.5.0/rl/marketdata/__init__.py +0 -0
  46. rl_pricing-0.5.0/rl/marketdata/sharadar.py +204 -0
  47. rl_pricing-0.5.0/rl/marketdata/synthetic.py +77 -0
  48. rl_pricing-0.5.0/rl/marketdata/yahoodownloader.py +130 -0
  49. rl_pricing-0.5.0/rl/model/__init__.py +0 -0
  50. rl_pricing-0.5.0/rl/model/models.py +584 -0
  51. rl_pricing-0.5.0/rl/preprocessing/__init__.py +0 -0
  52. rl_pricing-0.5.0/rl/preprocessing/data.py +26 -0
  53. rl_pricing-0.5.0/rl/preprocessing/preprocessors.py +248 -0
  54. rl_pricing-0.5.0/rl/trade/__init__.py +0 -0
  55. rl_pricing-0.5.0/rl/trade/backtest.py +93 -0
  56. rl_pricing-0.5.0/rl/trade/baselines.py +229 -0
  57. rl_pricing-0.5.0/rl/trade/execution.py +48 -0
  58. rl_pricing-0.5.0/rl/trade/metrics.py +154 -0
  59. rl_pricing-0.5.0/rl_pricing.egg-info/PKG-INFO +86 -0
  60. rl_pricing-0.5.0/rl_pricing.egg-info/SOURCES.txt +93 -0
  61. rl_pricing-0.5.0/rl_pricing.egg-info/dependency_links.txt +1 -0
  62. rl_pricing-0.5.0/rl_pricing.egg-info/entry_points.txt +2 -0
  63. rl_pricing-0.5.0/rl_pricing.egg-info/requires.txt +14 -0
  64. rl_pricing-0.5.0/rl_pricing.egg-info/top_level.txt +1 -0
  65. rl_pricing-0.5.0/ruff.toml +7 -0
  66. rl_pricing-0.5.0/setup.cfg +4 -0
  67. rl_pricing-0.5.0/setup.py +93 -0
  68. rl_pricing-0.5.0/tests/conftest.py +40 -0
  69. rl_pricing-0.5.0/tests/test_backtest_fees.py +87 -0
  70. rl_pricing-0.5.0/tests/test_bench_baselines.py +293 -0
  71. rl_pricing-0.5.0/tests/test_bench_core.py +133 -0
  72. rl_pricing-0.5.0/tests/test_bench_data.py +265 -0
  73. rl_pricing-0.5.0/tests/test_bench_forecast.py +148 -0
  74. rl_pricing-0.5.0/tests/test_bench_ledger.py +507 -0
  75. rl_pricing-0.5.0/tests/test_bench_monthly.py +204 -0
  76. rl_pricing-0.5.0/tests/test_bench_rl.py +224 -0
  77. rl_pricing-0.5.0/tests/test_bench_runner.py +320 -0
  78. rl_pricing-0.5.0/tests/test_bench_scoring.py +233 -0
  79. rl_pricing-0.5.0/tests/test_bench_sweep.py +232 -0
  80. rl_pricing-0.5.0/tests/test_bench_tradingagents.py +332 -0
  81. rl_pricing-0.5.0/tests/test_bench_verdicts.py +232 -0
  82. rl_pricing-0.5.0/tests/test_cli.py +86 -0
  83. rl_pricing-0.5.0/tests/test_data_eligibility.py +193 -0
  84. rl_pricing-0.5.0/tests/test_ensemble_options.py +306 -0
  85. rl_pricing-0.5.0/tests/test_env_stocktrading.py +227 -0
  86. rl_pricing-0.5.0/tests/test_evaluation.py +40 -0
  87. rl_pricing-0.5.0/tests/test_forecast.py +163 -0
  88. rl_pricing-0.5.0/tests/test_metrics_and_baselines.py +127 -0
  89. rl_pricing-0.5.0/tests/test_min_variance.py +167 -0
  90. rl_pricing-0.5.0/tests/test_other_envs.py +185 -0
  91. rl_pricing-0.5.0/tests/test_pipeline.py +350 -0
  92. rl_pricing-0.5.0/tests/test_preprocessors.py +151 -0
  93. rl_pricing-0.5.0/tests/test_sharadar.py +134 -0
  94. rl_pricing-0.5.0/tests/test_validation.py +92 -0
  95. rl_pricing-0.5.0/tests/test_yahoodownloader.py +152 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021-2026 Bicheng Wang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ include main.py
2
+ include pytest.ini
3
+ include requirements.in
4
+ include requirements.txt
5
+ include ruff.toml
6
+ recursive-include tests *.py
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: rl-pricing
3
+ Version: 0.5.0
4
+ Summary: A reproducibility lab for trading AI: causal backtests, a live forward-test track and a pre-open decision ledger for RL, LLM and forecasting policies.
5
+ Home-page: https://github.com/BichengWang/RL-pricing
6
+ Author: Bicheng Wang
7
+ License-Expression: MIT
8
+ Project-URL: Source, https://github.com/BichengWang/RL-pricing
9
+ Project-URL: Issues, https://github.com/BichengWang/RL-pricing/issues
10
+ Project-URL: Leaderboard, https://github.com/BichengWang/RL-pricing/blob/main/leaderboard/LEADERBOARD.md
11
+ Project-URL: Paper, https://arxiv.org/abs/2609.31870
12
+ Keywords: reinforcement learning,portfolio management,backtesting,benchmark,leaderboard
13
+ Platform: any
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: Implementation :: CPython
23
+ Classifier: Topic :: Office/Business :: Financial :: Investment
24
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: numpy>=1.24
29
+ Requires-Dist: pandas>=2.0
30
+ Requires-Dist: stockstats>=0.6.2
31
+ Requires-Dist: yfinance>=0.2.40
32
+ Requires-Dist: matplotlib>=3.7
33
+ Requires-Dist: gymnasium>=0.29.1
34
+ Requires-Dist: stable-baselines3>=2.3.2
35
+ Requires-Dist: torch>=2.0
36
+ Provides-Extra: test
37
+ Requires-Dist: pytest>=7.0; extra == "test"
38
+ Provides-Extra: lint
39
+ Requires-Dist: ruff>=0.5; extra == "lint"
40
+ Dynamic: author
41
+ Dynamic: classifier
42
+ Dynamic: description
43
+ Dynamic: description-content-type
44
+ Dynamic: home-page
45
+ Dynamic: keywords
46
+ Dynamic: license-file
47
+ Dynamic: license-expression
48
+ Dynamic: platform
49
+ Dynamic: project-url
50
+ Dynamic: provides-extra
51
+ Dynamic: requires-dist
52
+ Dynamic: requires-python
53
+ Dynamic: summary
54
+
55
+ # RL-pricing
56
+
57
+ A reproducibility lab for trading AI. RL-pricing re-tests published claims
58
+ about deep RL agents, LLM trading agents and forecasters under one
59
+ pre-registered protocol. This package holds the lab's instruments:
60
+
61
+ - `rl-pricing bench`: a causal backtesting benchmark. Policies decide at the
62
+ close and fill at the next open with fees; learned policies run over
63
+ several seeds; frozen policies are scored on a live forward-test track; and
64
+ a decision ledger records LLM agents' trades before each US market open.
65
+ - `rl-pricing --mode train|backtest|ensemble|lstm`: the share-trading toolkit
66
+ that trains and backtests Stable-Baselines3 agents (A2C, PPO, DDPG, TD3,
67
+ SAC), a rolling ensemble and an LSTM forecaster on daily equity data.
68
+
69
+ ```bash
70
+ pip install rl-pricing
71
+ rl-pricing bench tasks # list the benchmark tasks
72
+ rl-pricing bench data us-sectors # download a task's data and print its hash
73
+ ```
74
+
75
+ The import package is `rl`, so this distribution can't share an environment
76
+ with the unrelated `rl` (GNU Readline bindings) distribution. Leaderboard
77
+ submissions, results and reports live in the
78
+ [repository](https://github.com/BichengWang/RL-pricing): clone it to run the
79
+ submissions, or pass `--submissions-root` to use your own.
80
+
81
+ - [Verdicts, findings and leaderboard](https://github.com/BichengWang/RL-pricing#readme)
82
+ - [Paper: Wang & Zhang, arXiv:2609.31870](https://arxiv.org/abs/2609.31870)
83
+
84
+ The training toolkit's environment, preprocessing and training code are
85
+ adapted from [FinRL](https://github.com/AI4Finance-Foundation/FinRL), which is
86
+ MIT-licensed.
@@ -0,0 +1,344 @@
1
+ # RL-pricing: a reproducibility lab for trading AI
2
+
3
+ [![tests](https://github.com/BichengWang/RL-pricing/actions/workflows/tests.yml/badge.svg)](https://github.com/BichengWang/RL-pricing/actions/workflows/tests.yml)
4
+ [![bench](https://github.com/BichengWang/RL-pricing/actions/workflows/bench.yml/badge.svg)](https://github.com/BichengWang/RL-pricing/actions/workflows/bench.yml)
5
+ [![arXiv](https://img.shields.io/badge/arXiv-2609.31870-b31b1b.svg)](https://arxiv.org/abs/2609.31870)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+
8
+ We re-test published trading-AI claims (deep RL, LLM agents, forecasters)
9
+ under one pre-registered protocol, and publish every verdict next to the
10
+ authors' reply. That includes the verdicts against our own paper. The instruments are public:
11
+ a causal backtesting benchmark, a live track scored only on market data that
12
+ did not exist when a policy was published, and a ledger of decisions
13
+ committed before each US market open.
14
+
15
+ ## Verdicts
16
+
17
+ <!-- reports:start -->
18
+ | Report | Claim | Level | Claimed | Ours (95% interval) | Verdict |
19
+ | --- | --- | --- | --- | --- | --- |
20
+ | [r0-wang-zhang-2026](reports/r0-wang-zhang-2026/report.md) | `ddpg-annual-return` (annual_return) | L2 | 55.5% | 24.6% [17.3%, 31.9%] | ✗ does not hold |
21
+ | [r0-wang-zhang-2026](reports/r0-wang-zhang-2026/report.md) | `ddpg-sharpe` (sharpe) | L2 | 1.38 | 1.16 [0.93, 1.39] | ◐ partly holds |
22
+ | [r0-wang-zhang-2026](reports/r0-wang-zhang-2026/report.md) | `td3-sharpe` (sharpe) | L2 | 1.37 | 1.10 [0.69, 1.52] | ◐ partly holds |
23
+ | [r0-wang-zhang-2026](reports/r0-wang-zhang-2026/report.md) | `sac-sharpe` (sharpe) | L2 | 1.33 | 1.23 [1.07, 1.39] | ◐ partly holds |
24
+ | [r0-wang-zhang-2026](reports/r0-wang-zhang-2026/report.md) | `ppo-sharpe` (sharpe) | L2 | 1.09 | 1.17 [0.90, 1.44] | ◐ partly holds |
25
+ | [r0-wang-zhang-2026](reports/r0-wang-zhang-2026/report.md) | `a2c-sharpe` (sharpe) | L2 | 0.82 | 1.09 [0.91, 1.27] | ◐ partly holds |
26
+ | [r0-wang-zhang-2026](reports/r0-wang-zhang-2026/report.md) | `lstm-max-drawdown` (max_drawdown) | L2 | -9.6% | -26.5% [-29.4%, -23.7%] | ? inconclusive |
27
+ | [r0-wang-zhang-2026](reports/r0-wang-zhang-2026/report.md) | `lstm-sharpe` (sharpe) | L2 | 1.19 | 0.75 [0.39, 1.12] | ? inconclusive |
28
+
29
+ Verdicts: ✅ holds, ◐ partly holds, ✗ does not hold, ? inconclusive. [How a verdict is decided](reports/README.md#how-a-verdict-is-decided).
30
+ <!-- reports:end -->
31
+
32
+ | Next | Claim under test | Due |
33
+ | --- | --- | --- |
34
+ | R1 | FinRL ensemble ([Yang et al., ICAIF 2020](https://arxiv.org/abs/2511.12120)): Sharpe 1.30 vs the DJIA's 0.47, 2016–2020 | 2026-12-01 |
35
+ | R2 | [TradingAgents](https://arxiv.org/abs/2412.20138) backtest: Sharpe 5.6–8.2 on AAPL, GOOGL, AMZN in 2024 Q1, replicated with a check for whether the models remember that period | 2027-01-05 |
36
+ | R3 | TradingAgents run live on the same three stocks for three months, each decision committed before the open ([pre-registration](reports/r3-tradingagents-live/preregistration.md)) | 2027-02-15 |
37
+
38
+ Authors get each draft seven days before publication. To nominate a claim,
39
+ open an issue titled `Nominate: <paper>` with the quoted claim and a link to
40
+ the code.
41
+
42
+ ## How the lab works
43
+
44
+ - **Pre-registered.** Each report's test plan is committed before anything
45
+ runs; the commit time is the evidence that the plan came first.
46
+ - **Three levels.** L1 replicates with the authors' code and setup. L2
47
+ re-tests under this repository's protocol: next-open fills, fees, at least
48
+ five seeds, survivorship-free data. L3 tries new windows and, for anything
49
+ that can run daily, the live ledger.
50
+ - **Verdicts by rule:** holds, partly holds, does not hold, or inconclusive,
51
+ decided by thresholds fixed in the pre-registration
52
+ ([rules and template](reports/README.md)).
53
+ - **LLM agents only count live.** Their historical backtests can be
54
+ contaminated by training data. The ledger commits each decision before the
55
+ open, and scoring checks the commit against GitHub's own run records.
56
+ - **Evidence is committed.** Each report ships its per-seed results, input
57
+ hashes and one command that reproduces them.
58
+
59
+ ## R0: our own paper
60
+
61
+ Full report: [`reports/r0-wang-zhang-2026/report.md`](reports/r0-wang-zhang-2026/report.md).
62
+
63
+ Our preprint ([Wang & Zhang, arXiv:2609.31870](https://arxiv.org/abs/2609.31870))
64
+ reported DDPG with a 55.5% annual return and Sharpe 1.38 on 20 large-cap
65
+ stocks in 2019–2020, with TD3 and SAC close behind. Those are single runs on a
66
+ universe picked in 2021 from the largest companies, which the paper itself
67
+ lists as limitations.
68
+
69
+ We re-ran it three ways, 5 seeds per agent, same 20 stocks, same 2019–2020
70
+ window, 0.1% fees:
71
+
72
+ - **Share env + crash rule**: the paper's own share-trading environment
73
+ (`python main.py --mode train`, now with next-open fills), including its
74
+ default rule that sells everything when a market-turbulence index spikes.
75
+ - **Share env, no rule**: the same with `--no-turbulence`.
76
+ - **Benchmark**: this repository's `paper-2021` task (portfolio weights, no
77
+ hand-coded exits).
78
+
79
+ Sharpe ratio, mean ± sd over 5 seeds [best seed]:
80
+
81
+ | Agent | Reported (1 run) | Share env + crash rule | Share env, no rule | Benchmark |
82
+ | --- | --- | --- | --- | --- |
83
+ | DDPG | 1.38 | 1.16 ± 0.19 [1.43] | 1.06 ± 0.18 [1.24] | 0.70 ± 0.17 [0.97] |
84
+ | TD3 | 1.37 | 1.10 ± 0.34 [1.37] | 1.00 ± 0.30 [1.37] | 0.70 ± 0.21 [1.01] |
85
+ | SAC | 1.33 | 1.23 ± 0.13 [1.42] | 1.23 ± 0.18 [1.49] | 0.66 ± 0.26 [0.82] |
86
+ | PPO | 1.09 | 1.17 ± 0.21 [1.32] | 0.95 ± 0.21 [1.21] | 0.98 ± 0.04 [1.02] |
87
+ | A2C | 0.82 | 1.09 ± 0.15 [1.27] | 0.97 ± 0.15 [1.16] | 1.03 ± 0.01 [1.05] |
88
+ | LSTM forecast | 1.19 | – | – | 0.75 ± 0.29 [1.08] |
89
+ | *1/N buy and hold* | – | *1.10* | *1.10* | *1.11* |
90
+
91
+ What holds up and what doesn't:
92
+
93
+ - **The 55.5% return does not.** The best of the 50 share-environment runs
94
+ earned 46% a year and the median 26%. Holding the 20 stocks in equal
95
+ weight earned 29.7%, so the hindsight-chosen universe and the 2019–2020
96
+ bull market account for most of the headline.
97
+ - **The Sharpe ratios are best-of-five, not typical.** Values like the
98
+ reported 1.33–1.38 show up as the best seed for most agents.
99
+ - **The crash rule is not learning.** With it on, all 25 runs were moved to
100
+ cash on the same five sessions of March 2020 by the environment, not the agent.
101
+ Switching it off drops every agent's mean Sharpe except SAC's below 1/N.
102
+ - **SAC is the one survivor, but not a significant one.** Without the rule
103
+ its seed-averaged Sharpe is 1.32 against 1/N's 1.10, alpha 7.5% a year. Over
104
+ a two-year window that is not significant (bootstrap p = 0.13, alpha
105
+ *t* = 1.6).
106
+ - **The benchmark environment flips the picture.** There the same agents
107
+ trail 1/N. Which difference drives that is open: share counts versus
108
+ weights, the features, or the share environment's per-trade limits.
109
+
110
+ Per-seed results, equity curves, commands and the input-data hash are in
111
+ [`reports/r0-wang-zhang-2026/evidence/`](reports/r0-wang-zhang-2026/evidence/).
112
+
113
+ ## Benchmark findings
114
+
115
+ ![Per-seed Sharpe ratios of every strategy against 1/N](leaderboard/sharpe.png)
116
+
117
+ Historical track, test windows 2018–2025 (US ETFs) and 2023–2025 (crypto),
118
+ Stable-Baselines3 agents with default and tuned hyperparameters, 5 seeds each:
119
+
120
+ - **No learned policy beats 1/N buy-and-hold here.** All 30 RL and LSTM entries
121
+ across the four tasks have a lower seed-averaged Sharpe ratio than holding
122
+ every asset in equal weight. Only 5 of their 150 individual seeds do better,
123
+ so cherry-picking one would make a publishable-looking result.
124
+ - **Tuning doesn't close the gap.** A declared sweep of 8 configurations each
125
+ for PPO and SAC, selected on validation data only, picks configurations that
126
+ still trail 1/N on every task. SAC's choice helps on the US tasks and
127
+ collapses on crypto (Sharpe 0.07 ± 0.49). The deflated Sharpe column
128
+ accounts for the 8 trials.
129
+ - **The off-policy agents churn themselves to death.** SAC, TD3 and DDPG
130
+ turn over 46–326× their equity per year. On the US tasks their alpha
131
+ against 1/N is negative, with *t*-statistics down to −2.8.
132
+ - **A2C rediscovers 1/N.** Its five seeds on US sectors land at Sharpe
133
+ 0.67 ± 0.01 against 1/N's 0.68: the softmax policy learns to stay close to
134
+ equal weights.
135
+ - **The one significant win is not RL.** In crypto, long-only minimum
136
+ variance beats 1/N (ΔSharpe +0.86, bootstrap p < 0.001), mostly by holding
137
+ the two least volatile coins, BTC (50% on average) and BNB (36%).
138
+
139
+ Better configurations are what the leaderboard is for: submit one, and state
140
+ how many you tried.
141
+
142
+ ## Leaderboard
143
+
144
+ <!-- leaderboard:start -->
145
+ | Track | Task | # | Strategy | Sharpe | CAGR | Max DD | ΔSharpe vs 1/N | DSR |
146
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- |
147
+ | historical | `crypto-majors` | 1 | Minimum variance | 1.64 | 90.9% | -33.3% | +0.86 (p<0.001) | 1.00 |
148
+ | historical | `crypto-majors` | 2 | Inverse volatility | 0.88 | 40.7% | -46.8% | +0.10 (p=0.089) | 0.94 |
149
+ | historical | `crypto-majors` | 3 | Momentum (top 3) | 0.84 | 37.8% | -47.9% | +0.06 (p=0.401) | 0.93 |
150
+ | historical | `crypto-majors` | 4 | **1/N buy and hold** ◆ | 0.78 | 32.7% | -44.5% | – | 0.91 |
151
+ | historical | `crypto-majors` | 6 | PPO (tuned) | 0.71 ± 0.22 | 26.9% | -49.3% | -0.04 (p=0.621) | 0.43 |
152
+ | historical | `us-multi-asset` | 1 | **1/N buy and hold** ◆ | 0.73 | 8.9% | -26.6% | – | 0.98 |
153
+ | historical | `us-multi-asset` | 2 | Momentum (top 3) | 0.72 | 9.9% | -23.7% | -0.01 (p=0.506) | 0.98 |
154
+ | historical | `us-multi-asset` | 3 | 1/N monthly rebalance | 0.69 | 8.0% | -25.8% | -0.04 (p=0.827) | 0.97 |
155
+ | historical | `us-multi-asset` | 4 | A2C | 0.66 ± 0.02 | 7.1% | -23.8% | -0.06 (p=0.940) | 0.97 |
156
+ | historical | `us-sectors` | 1 | 1/N monthly rebalance | 0.69 | 11.6% | -36.7% | +0.01 (p=0.449) | 0.97 |
157
+ | historical | `us-sectors` | 2 | **1/N buy and hold** ◆ | 0.68 | 11.5% | -35.5% | – | 0.97 |
158
+ | historical | `us-sectors` | 3 | A2C | 0.67 ± 0.01 | 10.3% | -34.3% | -0.01 (p=0.650) | 0.97 |
159
+ | historical | `paper-2021` (re-evaluation) | – | Momentum (top 3) | 1.31 | 48.6% | -30.4% | +0.20 (p=0.240) | 0.96 |
160
+ | historical | `paper-2021` (re-evaluation) | – | **1/N buy and hold** ◆ | 1.11 | 29.9% | -29.4% | – | 0.94 |
161
+ | historical | `paper-2021` (re-evaluation) | – | 1/N monthly rebalance | 1.04 | 26.7% | -29.5% | -0.07 (p=0.777) | 0.93 |
162
+ | historical | `paper-2021` (re-evaluation) | – | A2C | 1.03 ± 0.01 | 25.1% | -28.2% | -0.08 (p=0.825) | 0.92 |
163
+
164
+ Full tables: [leaderboard/LEADERBOARD.md](leaderboard/LEADERBOARD.md).
165
+ <!-- leaderboard:end -->
166
+
167
+ ## Why trust these numbers
168
+
169
+ | | This benchmark | Common in RL-trading papers |
170
+ | --- | --- | --- |
171
+ | Execution | Decide at the close, fill at the next open, proportional fees | Fill at the same close used to decide; often no fees |
172
+ | Universe | Fixed ETFs; crypto top-10 by volume *before* the data starts, delistings included | Today's index members, back-tested into the past |
173
+ | Randomness | 5 seeds per learned policy, mean ± sd, every seed plotted | One run |
174
+ | Model selection | Validation window before the test; declared `trials` deflate the Sharpe | Tuned on the test period |
175
+ | Out-of-sample | Live track: frozen policy, scored only on sessions after its public freeze | One fixed historical window |
176
+ | Metrics | One implementation for everyone; beta and alpha against 1/N | Per-paper definitions |
177
+
178
+ The live track is the part nobody can overfit. A policy is fitted on all
179
+ data available today and its artifacts are committed with SHA-256 hashes. A
180
+ CI job on `main` then stamps the time the freeze became public
181
+ (`leaderboard/publications.json`, which pull requests may not edit), and
182
+ from then on it is scored monthly on sessions that start after that stamp.
183
+ The first live scores appear after the next monthly run.
184
+
185
+ ## Quick start
186
+
187
+ Python 3.10 or newer:
188
+
189
+ ```bash
190
+ git clone https://github.com/BichengWang/RL-pricing.git
191
+ cd RL-pricing
192
+ python -m venv .venv && source .venv/bin/activate
193
+ python -m pip install -e .
194
+
195
+ rl-pricing bench tasks # list tasks
196
+ rl-pricing bench run --task synthetic --submission momentum --out-root /tmp/board
197
+ rl-pricing bench run --task synthetic --submission ppo --seeds 2 --out-root /tmp/board
198
+ rl-pricing bench leaderboard --out-root /tmp/board --no-readme
199
+ ```
200
+
201
+ The `synthetic` task is offline random walks with no edge by construction.
202
+ Real tasks download data on first use (`rl-pricing bench data us-sectors`)
203
+ into `datasets/bench/`. PPO takes one to two minutes per seed on a laptop CPU.
204
+
205
+ ## Submit a policy
206
+
207
+ A policy maps the history up to today's close to long-only target weights,
208
+ which fill at the next open. Here is a complete one:
209
+
210
+ ```python
211
+ # submissions/low-vol-half/policy.py
212
+ import numpy as np
213
+
214
+ from rl.bench.policy import Policy
215
+
216
+
217
+ class LowVolHalf(Policy):
218
+ """Equal-weight the calmer half of the listed assets."""
219
+
220
+ def act(self, view):
221
+ closes = view.history.window(64).close
222
+ vol = np.nanstd(np.diff(np.log(closes), axis=0), axis=0)
223
+ vol = np.where(view.listed & np.isfinite(vol), vol, np.inf)
224
+ keep = np.argsort(vol)[: max(1, view.listed.sum() // 2)]
225
+ weights = np.zeros(len(view.tickers))
226
+ weights[keep] = 1 / len(keep)
227
+ return weights
228
+ ```
229
+
230
+ ```json
231
+ {
232
+ "name": "Low-vol half",
233
+ "kind": "other",
234
+ "policy": "policy.py:LowVolHalf",
235
+ "trials": 3,
236
+ "authors": ["Your Name"],
237
+ "description": "Equal-weight the half of the assets with the lowest 63-day volatility."
238
+ }
239
+ ```
240
+
241
+ Learning policies implement `fit(history, validation_start, seed)` as well:
242
+ train on sessions before `validation_start`, select on the rest, and set
243
+ `stochastic = True` to be evaluated over several seeds. They also implement
244
+ `save`/`load` so the frozen live version can be restored. See
245
+ [`rl/bench/policies/sb3.py`](rl/bench/policies/sb3.py) for a full example.
246
+ Set `trials` honestly to the number of configurations you tried.
247
+
248
+ Then:
249
+
250
+ 1. `rl-pricing bench validate` and `rl-pricing bench run --task <task> --submission <id>` locally.
251
+ 2. Open a pull request with `submissions/<id>/` and your `leaderboard/historical/` records.
252
+ CI checks that the submission builds and the baselines reproduce.
253
+ 3. On merge, CI freezes the policy on today's data (or publishes your
254
+ committed freeze from `rl-pricing bench freeze`) and the monthly job scores
255
+ it live from then on.
256
+
257
+ ## Tasks
258
+
259
+ | Task | Universe | Test window | Fee per side | Why it is survivorship-free |
260
+ | --- | --- | --- | --- | --- |
261
+ | `us-sectors` | 9 SPDR sector ETFs | 2018–2025 | 5 bps | all listed since 1998 |
262
+ | `us-multi-asset` | SPY, QQQ, IWM, EFA, EEM, TLT, IEF, LQD, GLD, VNQ | 2018–2025 | 5 bps | all listed since 2004 |
263
+ | `crypto-majors` | top 10 Binance USDT pairs by Q2-2018 volume | 2023–2025 | 10 bps | chosen before the data starts; BCC and EOS delist mid-sample |
264
+ | `paper-2021` | the 2021 paper's 20 large caps | 2019–2020 | 10 bps | *it is not*: shown as a re-evaluation, not ranked |
265
+ | `synthetic` | 5 random walks | 2018–2019 | 10 bps | offline demo |
266
+
267
+ Each task trains before its validation window, selects on validation, and
268
+ reports the test window. Definitions live in [`rl/bench/tasks.py`](rl/bench/tasks.py).
269
+
270
+ ## How scoring works
271
+
272
+ - **Execution** ([`rl/bench/engine.py`](rl/bench/engine.py)): targets set at
273
+ close *t* fill at open *t+1* with fees solved exactly on traded notional.
274
+ Weights are long-only and sum to at most one; the rest is cash at 0%. A
275
+ delisted holding is sold at its last close. RL agents train in a gym
276
+ environment that uses the same `Account`, so training and evaluation
277
+ accounting are identical.
278
+ - **Metrics** ([`rl/bench/scoring.py`](rl/bench/scoring.py)): Sharpe, CAGR,
279
+ max drawdown, annual turnover, and beta/alpha against 1/N (Newey-West *t*).
280
+ ΔSharpe against 1/N comes with a paired stationary-bootstrap p-value. The
281
+ deflated Sharpe ratio (Bailey & López de Prado) accounts for declared
282
+ `trials`. Full definitions are in [`leaderboard/LEADERBOARD.md`](leaderboard/LEADERBOARD.md).
283
+ - **Records**: every run stores its daily equity curve per seed, data
284
+ SHA-256, software versions and commit under `leaderboard/`, so all
285
+ metrics can be recomputed.
286
+
287
+ ## Data
288
+
289
+ US tasks use Yahoo Finance prices and dividends, downloaded to a local cache.
290
+ Yahoo's terms prohibit redistribution, so the repository ships no prices,
291
+ only derived results. Prices are adjusted for dividends forward from each
292
+ task's first session rather than taken from Yahoo's adjusted close, whose
293
+ last digits change between identical requests. A past session therefore
294
+ keeps its prices as new sessions arrive, and replaying the same window
295
+ reproduces its data hash; a changed hash means Yahoo revised its
296
+ history. Crypto uses
297
+ [Binance Vision](https://data.binance.vision) daily klines, each archive
298
+ verified against Binance's SHA-256 checksum. Binance Vision data is licensed
299
+ CC BY-NC-SA 4.0; the crypto results here derive from it, with attribution,
300
+ for non-commercial research.
301
+
302
+ ## Limitations
303
+
304
+ - Fees are proportional; spread, market impact and borrowing costs are not
305
+ modeled, and cash earns nothing.
306
+ - Daily bars only; adjusted prices model reinvested distributions rather
307
+ than exact exchange fills.
308
+ - Eight-year and three-year test windows are a few market regimes, which is
309
+ why the live track exists.
310
+ - Reference RL agents are untuned and use one feature set
311
+ ([`rl/bench/features.py`](rl/bench/features.py)); they are a floor, not
312
+ the best RL can do.
313
+
314
+ ## Training toolkit
315
+
316
+ The original FinRL-style training pipeline (share trading, portfolio
317
+ allocation, LSTM forecaster, rolling ensemble via `python main.py --mode ...`)
318
+ is still available and documented in [`docs/toolkit.md`](docs/toolkit.md).
319
+
320
+ ## Development
321
+
322
+ ```bash
323
+ pip install -e ".[test,lint]"
324
+ python -m pytest # offline: synthetic data and fixtures
325
+ ruff check .
326
+ ```
327
+
328
+ Use `type(scope): title` for pull request titles. The
329
+ [PR template](.github/pull_request_template.md) asks for a short summary and
330
+ verification results.
331
+
332
+ ## Citing
333
+
334
+ GitHub's "Cite this repository" uses [`CITATION.cff`](CITATION.cff). The
335
+ original study is Wang & Zhang, *Deep Reinforcement Learning for Equity
336
+ Trading: Benchmarking Actor-Critic Methods with Forward Retraining*,
337
+ [arXiv:2609.31870](https://arxiv.org/abs/2609.31870)
338
+ ([local PDF](docs/Wang_Zhang_2021_Deep_Learning_Stock_Trading_arxiv.pdf)).
339
+
340
+ ## License
341
+
342
+ MIT; see [LICENSE](LICENSE). The training toolkit's environment,
343
+ preprocessing and training code are adapted from the MIT-licensed
344
+ [FinRL](https://github.com/AI4Finance-Foundation/FinRL) library.
@@ -0,0 +1,6 @@
1
+ """Command-line entry point; see ``python main.py --help``."""
2
+
3
+ from rl.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,4 @@
1
+ [build-system]
2
+ # 77 is the first release that writes License-Expression (PEP 639) metadata.
3
+ requires = ["setuptools>=77"]
4
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,3 @@
1
+ [pytest]
2
+ testpaths = tests
3
+ pythonpath = . tests
@@ -0,0 +1,15 @@
1
+ # data handling
2
+ numpy>=1.24
3
+ pandas>=2.0
4
+ stockstats>=0.6.2
5
+ yfinance>=0.2.40
6
+
7
+ # plot
8
+ matplotlib>=3.7
9
+
10
+ # Model Building Requirements
11
+ gymnasium>=0.29.1
12
+ stable-baselines3>=2.3.2
13
+ torch>=2.0
14
+
15
+ # Test and lint tools are extras in setup.py: pip install -e ".[test]"
@@ -0,0 +1,14 @@
1
+ # Versions the test suite was last run with (Python 3.13).
2
+ # requirements.in lists the supported ranges; setup.py installs from it.
3
+ #
4
+ # For a CPU-only PyTorch (much smaller download), install it first:
5
+ # pip install torch --index-url https://download.pytorch.org/whl/cpu
6
+ gymnasium==1.4.0
7
+ matplotlib==3.11.2
8
+ numpy==2.5.3
9
+ pandas==3.0.5
10
+ pytest==9.1.1
11
+ stable-baselines3==2.9.0
12
+ stockstats==0.6.9
13
+ torch==2.14.1
14
+ yfinance==1.7.0
File without changes
File without changes
@@ -0,0 +1,124 @@
1
+ """Driver for the R1 runs: the toolkit's ensemble with the FinRL authors' recipe.
2
+
3
+ Sets what the toolkit lets us match from Yang, Liu, Zhong and Walid (ICAIF
4
+ 2020): the four indicators (``macd``, ``rsi_30``, ``cci_30``, ``dx_30``), the
5
+ training steps per algorithm (A2C 30,000, PPO 100,000, DDPG 10,000), PPO with
6
+ ``ent_coef`` 0.005 and 128-step rollouts in minibatches of 16 (the authors'
7
+ ``nminibatches=8``), and DDPG with Ornstein-Uhlenbeck action noise of sigma
8
+ 0.5. A2C and the rest of DDPG keep the toolkit defaults, as does everything
9
+ else (0.1% fees per side, $1,000,000, ``hmax`` 100, reward scaling 1e-4,
10
+ 63-session rebalance and validation windows, next-open fills, observation
11
+ normalisation, refit of the winner). Each algorithm also trades its own
12
+ account (``standalone_accounts``).
13
+
14
+ Prices come from a CSV in the toolkit format (``date,tic,open,high,low,close,
15
+ volume``), e.g. from :mod:`rl.marketdata.sharadar`::
16
+
17
+ python -m rl.autotrain.r1 --data-file prices.csv --seed 0 --run-name r1_a_s0
18
+ """
19
+
20
+ import argparse
21
+ import copy
22
+ import logging
23
+
24
+ from rl.autotrain.training import TrainConfig, run_ensemble
25
+ from rl.config import config
26
+
27
+ AUTHORS_INDICATORS = ("macd", "rsi_30", "cci_30", "dx_30")
28
+ AUTHORS_TIMESTEPS = {"a2c": 30_000, "ppo": 100_000, "ddpg": 10_000}
29
+ AUTHORS_DDPG_NOISE_SIGMA = 0.5
30
+
31
+
32
+ def authors_model_kwargs(ddpg_noise_sigma=AUTHORS_DDPG_NOISE_SIGMA):
33
+ """Per-algorithm model settings (each replaces the config.py default)."""
34
+ return {
35
+ "a2c": copy.deepcopy(config.A2C_PARAMS),
36
+ # 128 steps in 8 minibatches; every other PPO setting is stable-baselines3's default.
37
+ "ppo": {"n_steps": 128, "batch_size": 16, "ent_coef": 0.005},
38
+ "ddpg": {
39
+ **copy.deepcopy(config.DDPG_PARAMS),
40
+ "action_noise": {"type": "ornstein_uhlenbeck", "sigma": ddpg_noise_sigma},
41
+ },
42
+ }
43
+
44
+
45
+ def r1_config(
46
+ data_file,
47
+ *,
48
+ seed,
49
+ use_turbulence=True,
50
+ ticker_list=None,
51
+ start_date="2009-01-02",
52
+ start_trade_date="2016-01-04",
53
+ end_date="2026-01-01",
54
+ ddpg_noise_sigma=AUTHORS_DDPG_NOISE_SIGMA,
55
+ ensemble_timesteps=None,
56
+ standalone_accounts=True,
57
+ run_name=None,
58
+ results_dir=config.RESULTS_DIR,
59
+ **overrides,
60
+ ):
61
+ """The :class:`TrainConfig` of one R1 ensemble run.
62
+
63
+ ``end_date`` is exclusive (the default includes 2025-12-31). ``overrides``
64
+ set any other :class:`TrainConfig` field.
65
+ """
66
+ settings = dict(
67
+ agent="ensemble",
68
+ task="trading",
69
+ data_source="csv",
70
+ data_file=data_file,
71
+ ticker_list=ticker_list,
72
+ start_date=start_date,
73
+ start_trade_date=start_trade_date,
74
+ end_date=end_date,
75
+ tech_indicator_list=list(AUTHORS_INDICATORS),
76
+ use_turbulence=use_turbulence,
77
+ ensemble_timesteps=dict(AUTHORS_TIMESTEPS if ensemble_timesteps is None else ensemble_timesteps),
78
+ ensemble_model_kwargs=authors_model_kwargs(ddpg_noise_sigma),
79
+ standalone_accounts=standalone_accounts,
80
+ seed=seed,
81
+ run_name=run_name,
82
+ results_dir=results_dir,
83
+ )
84
+ settings.update(overrides)
85
+ return TrainConfig(**settings).validate()
86
+
87
+
88
+ def run_r1(data_file, *, seed, **options):
89
+ """Run the ensemble for ``seed`` (see :func:`r1_config`); returns :func:`run_ensemble`'s dict."""
90
+ return run_ensemble(r1_config(data_file, seed=seed, **options))
91
+
92
+
93
+ def main(argv=None):
94
+ parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
95
+ parser.add_argument("--data-file", required=True, help="CSV of date,tic,open,high,low,close,volume")
96
+ parser.add_argument("--seed", type=int, required=True)
97
+ parser.add_argument("--tickers", nargs="+", help="default: every ticker in the file")
98
+ parser.add_argument("--no-turbulence", action="store_true", help="arm B: no liquidation rule")
99
+ parser.add_argument("--start-date", default="2009-01-02")
100
+ parser.add_argument("--start-trade-date", default="2016-01-04")
101
+ parser.add_argument("--end-date", default="2026-01-01", help="exclusive")
102
+ parser.add_argument("--ddpg-noise-sigma", type=float, default=AUTHORS_DDPG_NOISE_SIGMA)
103
+ parser.add_argument("--run-name")
104
+ parser.add_argument("--results-dir", default=config.RESULTS_DIR)
105
+ options = parser.parse_args(argv)
106
+ logging.basicConfig(
107
+ level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s"
108
+ )
109
+ return run_r1(
110
+ options.data_file,
111
+ seed=options.seed,
112
+ use_turbulence=not options.no_turbulence,
113
+ ticker_list=options.tickers,
114
+ start_date=options.start_date,
115
+ start_trade_date=options.start_trade_date,
116
+ end_date=options.end_date,
117
+ ddpg_noise_sigma=options.ddpg_noise_sigma,
118
+ run_name=options.run_name,
119
+ results_dir=options.results_dir,
120
+ )
121
+
122
+
123
+ if __name__ == "__main__":
124
+ main()