|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +This file guides Codex (Codex.ai/code) when working in this repository. |
| 4 | +It is kept deliberately factual — every claim below was verified against the |
| 5 | +source tree. When you change something structural, update this file. |
| 6 | + |
| 7 | +## Project Overview |
| 8 | + |
| 9 | +Backtrader is a Python algorithmic-trading backtesting framework supporting |
| 10 | +low-, mid-, and high-frequency strategy development, backtesting, and live |
| 11 | +trading. This repo is a performance-oriented fork of the original |
| 12 | +[backtrader](https://www.backtrader.com/) that **removes metaclass-based |
| 13 | +metaprogramming** in favor of explicit mixin + factory initialization while |
| 14 | +keeping the public API compatible. |
| 15 | + |
| 16 | +- **Version**: `1.1.0` (see `backtrader/version.py`) |
| 17 | +- **License**: GPLv3 |
| 18 | +- **Python**: 3.8–3.13 (classifiers in `setup.py`; 3.11 recommended) |
| 19 | +- **Not on PyPI** — install from source only. |
| 20 | + |
| 21 | +### Branch context |
| 22 | + |
| 23 | +- `dev` — active development; the canonical branch. All work lands here first. |
| 24 | +- `master` — stable, aligned with upstream behavior. Used as the correctness |
| 25 | + baseline (regression tests bake master's metrics as expected values). |
| 26 | +- Other branches (`crypto`, `ctp`, `dev_cython`, `development`, etc.) are |
| 27 | + feature/experiment branches; do not target them unless asked. |
| 28 | + |
| 29 | +> **Do not push directly to `master`.** Push to `dev`. `git push` is configured |
| 30 | +> to push to both GitHub (`cloudQuant/backtrader`) and Gitee |
| 31 | +> (`yunjinqi/backtrader`) remotes. |
| 32 | +
|
| 33 | +## Implementation status / reality checks |
| 34 | + |
| 35 | +These correct common stale assumptions — verify before relying on docs: |
| 36 | + |
| 37 | +- **Pure Python today.** Although `cython>=0.29.0` is a declared dependency and |
| 38 | + older docs reference `compile_cython_numba_files.py`, there are currently |
| 39 | + **no tracked `.pyx` files and no `ext_modules` in `setup.py`**. `pip install` |
| 40 | + builds a pure-Python package. The only native-ish acceleration in the tree is |
| 41 | + `numba` used inside `backtrader/utils/dateintern.py`. Do not assume a Cython |
| 42 | + build step is required or present. |
| 43 | +- **Metaclasses are gone.** Object construction goes through |
| 44 | + `metabase.ObjectFactory` / `BaseMixin.donew` and `ParamsMixin.__init_subclass__` |
| 45 | + (a `patched_init` wrapper), not a metaclass `__call__`. |
| 46 | +- File sizes below are real line counts, not the inflated numbers in earlier |
| 47 | + revisions of this doc. |
| 48 | + |
| 49 | +## Development commands |
| 50 | + |
| 51 | +### Install |
| 52 | + |
| 53 | +```bash |
| 54 | +pip install -r requirements.txt # core + dev deps |
| 55 | +pip install -U . # build & install |
| 56 | +pip install -e . # editable/dev install |
| 57 | +``` |
| 58 | + |
| 59 | +No separate Cython compile step is needed for a normal install. |
| 60 | + |
| 61 | +### Testing (tiered — see `Makefile` and `conftest.py`) |
| 62 | + |
| 63 | +The strategy regression suite is large (~10 min full). Tests are split into |
| 64 | +tiers by **measured per-file duration**, applied dynamically at collection time |
| 65 | +(no test files are edited): |
| 66 | + |
| 67 | +```bash |
| 68 | +make test-fast # ~3.5 min: all non-strategy tests + fastest ~35% of |
| 69 | + # strategy tests. Daily "did I break anything" loop. |
| 70 | + # == pytest tests -m "not slow" -n 8 -q |
| 71 | +make test-slow # the slowest ~65% strategy tests test-fast skips |
| 72 | +make test-strategies # all 1,271 strategy regression tests (~9 min) |
| 73 | +make test-all # entire suite in parallel (~10 min) |
| 74 | +make test-coverage # coverage report |
| 75 | + |
| 76 | +# Single test, verbose: |
| 77 | +pytest tests/path/to/test_file.py::test_name -v --tb=short |
| 78 | +``` |
| 79 | + |
| 80 | +How the split works: |
| 81 | + |
| 82 | +- `conftest.py::pytest_collection_modifyitems` reads |
| 83 | + `tests/functional/strategies/.test_durations.json` (committed), computes the |
| 84 | + `BT_SLOW_PERCENTILE`th percentile (default **35**) of recorded durations, and |
| 85 | + tags any strategy file at/above it with the existing `slow` marker. |
| 86 | +- **Unknown/new files default to the FAST tier**, so newly added or regenerated |
| 87 | + tests always run on `test-fast` — exactly what you want for catching new bugs. |
| 88 | +- Tune coverage vs speed: `BT_SLOW_PERCENTILE=25 make test-fast` (faster) … |
| 89 | + `=50` (broader). |
| 90 | +- Refresh timings after adding/removing strategy tests: |
| 91 | + `python scripts/refresh_strategy_durations.py`. |
| 92 | + |
| 93 | +### Choosing which `backtrader` to test against |
| 94 | + |
| 95 | +Running pytest from the repo root resolves `import backtrader` to the **local |
| 96 | +repo copy** by default. To test the installed site-packages copy instead: |
| 97 | + |
| 98 | +```bash |
| 99 | +BACKTRADER_USE_INSTALLED=1 pytest ... # env var |
| 100 | +pytest ... --use-installed-backtrader # CLI flag |
| 101 | +``` |
| 102 | + |
| 103 | +The active `backtrader.__file__` is printed in the pytest session header. The |
| 104 | +switch works under `pytest-xdist` parallel mode. Logic lives in `conftest.py`. |
| 105 | + |
| 106 | +### Code quality |
| 107 | + |
| 108 | +```bash |
| 109 | +make format # black, line-length 100 |
| 110 | +make format-check |
| 111 | +make lint # ruff |
| 112 | +make type-check # mypy |
| 113 | +make security # bandit |
| 114 | +make quality-check # all of the above (no tests) |
| 115 | +bash scripts/optimize_code.sh # pyupgrade + isort + black + ruff + tests |
| 116 | +``` |
| 117 | + |
| 118 | +### Docs & utilities |
| 119 | + |
| 120 | +```bash |
| 121 | +make docs / docs-en / docs-zh # Sphinx docs (English + Chinese) |
| 122 | +make help # list all make targets |
| 123 | +make clean # clean build artifacts |
| 124 | +``` |
| 125 | + |
| 126 | +## Architecture |
| 127 | + |
| 128 | +### Construction pipeline (replaces the old metaclass) |
| 129 | + |
| 130 | +Object creation flows through `backtrader/metabase.py`: |
| 131 | + |
| 132 | +- `ObjectFactory.create(cls, *args, **kwargs)` runs the lifecycle hooks: |
| 133 | + `doprenew → donew → dopreinit → doinit → dopostinit`. |
| 134 | +- `BaseMixin` provides default `donew/dopreinit/doinit/dopostinit`. |
| 135 | +- `ParamsMixin.__init_subclass__` installs a `patched_init` wrapper on each |
| 136 | + subclass's `__init__` that wires up `self.p`/`self.params`, sets `data0/data1` |
| 137 | + aliases, and runs the lifecycle. **Most indicators are constructed through |
| 138 | + this `patched_init` path, not `ObjectFactory.create` directly.** |
| 139 | +- Owner discovery uses `metabase.OwnerContext` (a context stack) and |
| 140 | + `metabase.findowner()` — the legacy stack-frame inspection is gone. |
| 141 | + |
| 142 | +> Key rule: call `super().__init__()` **before** accessing `self.p`/`self.params` |
| 143 | +> or lines. Never reintroduce a metaclass — use mixins + `donew()`. |
| 144 | +
|
| 145 | +### Line system (bottom-up) |
| 146 | + |
| 147 | +`LineRoot → LineBuffer → LineSeries → LineIterator` |
| 148 | + |
| 149 | +- `lineroot.py` — base interfaces, period management, stage1/stage2. |
| 150 | +- `linebuffer.py` (~2,800 lines) — circular-buffer line storage; also defines |
| 151 | + `LineActions` / `LinesOperation` (the objects produced by expressions like |
| 152 | + `(data.high + data.low) / 2.0`). |
| 153 | +- `lineseries.py` (~2,450 lines) — `Lines`/`LineSeries`, `LineSeriesStub`, |
| 154 | + `LineSeriesMaker`. |
| 155 | +- `lineiterator.py` (~2,920 lines) — `LineIterator`, `IndicatorBase`, |
| 156 | + `DataAccessor`; iteration phases and the `_clock` resolution helpers |
| 157 | + (`_line_like_source_clock`, `_resolve_authoritative_buflen`, |
| 158 | + `_ensure_lineactions_inputs_computed`). |
| 159 | + |
| 160 | +Access patterns: `data.close[0]` (current bar), `data.close[-1]` (previous). |
| 161 | + |
| 162 | +### Components (all extend LineIterator) |
| 163 | + |
| 164 | +- `indicator.py` (`Indicator`, `_ltype=IndType=0`) + `indicators/` (50 files). |
| 165 | +- `observer.py` + `observers/` — chart observers; notably |
| 166 | + `observers/trade_logger.py` (`TradeLogger`) for JSON order/trade/signal/ |
| 167 | + position logs (used by the branch-compare tooling). |
| 168 | +- `analyzer.py` + `analyzers/` (17 files) — Sharpe, drawdown, returns, SQN, … |
| 169 | +- `sizer.py` + `sizers/`, `signal.py` + `signals/`, `comminfo.py` + |
| 170 | + `commissions/`. |
| 171 | + |
| 172 | +### Data, broker, engine |
| 173 | + |
| 174 | +- `feed.py` + `feeds/` (17 files) — CSV, pandas, IB, CCXT, etc.; |
| 175 | + `resamplerfilter.py` for resample/replay. |
| 176 | +- `broker.py` + `brokers/` — order matching and portfolio state. |
| 177 | +- `cerebro.py` (~2,440 lines) — orchestrator. `run()` → `runstrategies()` → |
| 178 | + `_runonce()` (vectorized) or `_runnext()` (event-driven). Tick-level mode is |
| 179 | + also supported. |
| 180 | + |
| 181 | +### Indicator registration & multi-data clocks (high-bug-risk area) |
| 182 | + |
| 183 | +- An indicator registers with its owner via `LineIterator.addindicator()` |
| 184 | + (`lineiterator.py:1584`), appending to `owner._lineiterators[ind._ltype]`. |
| 185 | + If an indicator isn't registered it won't update during the run. |
| 186 | +- **Multi-timeframe gotcha:** an indicator built on a secondary feed — e.g. |
| 187 | + `SMA((h1.high + h1.low)/2.0)` or `EMA(EMA(h4.close))` inside an M15 strategy — |
| 188 | + must advance on the *secondary* feed's clock, not the strategy's primary feed. |
| 189 | + In runonce mode this is handled in `Strategy._periodset()`, which resolves each |
| 190 | + indicator's data dependency to its concrete feed and pins |
| 191 | + `indicator._resolved_secondary_clock`; the post-phase advance loop in |
| 192 | + `_oncepost()` and `Indicator.advance()` honor that clock. See |
| 193 | + `docs/DEV_REGRESSION_FAILURES.md` for the full diagnosis of the bug class this |
| 194 | + fixes. When touching clock/minperiod logic, run `make test-strategies` — these |
| 195 | + multi-data cases are exactly what regress. |
| 196 | + |
| 197 | +### Execution phases |
| 198 | + |
| 199 | +`prenext` (before minperiod) → `nextstart` (minperiod first met) → `next` |
| 200 | +(normal). Vectorized mode uses `once()` (`preonce`/`oncestart`/`once`) to fill |
| 201 | +whole line arrays in batch, then replays per bar. |
| 202 | + |
| 203 | +### Data flow |
| 204 | + |
| 205 | +```text |
| 206 | +Data Feed(s) → Cerebro → Strategy → Indicators / Observers / Analyzers |
| 207 | + ↓ |
| 208 | + Broker ← Orders |
| 209 | +``` |
| 210 | + |
| 211 | +## Special modes |
| 212 | + |
| 213 | +- **TS (time series)** and **CS (cross-section)** modes for multi-asset |
| 214 | + portfolio backtests (`utils/` helpers; some docs reference dedicated value |
| 215 | + calculators — confirm presence before relying on them). |
| 216 | +- Multiple plotting backends: Plotly (`plot/`), Bokeh (`bokeh/`), Matplotlib. |
| 217 | +- Report generation: `reports/` (`reporter.py`, `performance.py`, `charts.py`). |
| 218 | + |
| 219 | +## Repository layout |
| 220 | + |
| 221 | +``` |
| 222 | +backtrader/ core library |
| 223 | + cerebro.py strategy.py indicator.py analyzer.py observer.py broker.py feed.py |
| 224 | + metabase.py parameters.py |
| 225 | + lineroot.py linebuffer.py lineseries.py lineiterator.py dataseries.py |
| 226 | + indicators/ analyzers/ observers/ feeds/ brokers/ filters/ sizers/ signals/ |
| 227 | + commissions/ stores/ channels/ mixins/ plot/ bokeh/ reports/ configs/ utils/ |
| 228 | +tests/ unit/ functional/ integration/ performance/ original_tests/ |
| 229 | + add_tests/ strategies/ bench/ datas/ fixtures/ factories/ test_utils/ |
| 230 | + functional/strategies/ 1,271 inlined regression tests in ~30 categories |
| 231 | +docs/ Sphinx docs (EN + ZH) + design/bug notes |
| 232 | +scripts/ optimize_code.sh, refresh_strategy_durations.py, |
| 233 | + run_strategy_branch_compare.py, … |
| 234 | +studies/ research/diagnostic scripts (e.g. branch_compare/) |
| 235 | +Makefile pyproject.toml setup.py pytest.ini requirements.txt conftest.py |
| 236 | +``` |
| 237 | + |
| 238 | +## Tests |
| 239 | + |
| 240 | +- `tests/functional/strategies/` holds 1,271 inlined regression tests across ~30 |
| 241 | + categories (trend_following, mean_reversion, asset_allocation, |
| 242 | + machine_learning, options, pairs_trading, …). Each is self-contained: inline |
| 243 | + strategy + data loader + `cerebro.run()` + assertions against master-baselined |
| 244 | + metrics. |
| 245 | +- `tests/unit/`, `tests/integration/`, `tests/performance/`, |
| 246 | + `tests/original_tests/`, `tests/add_tests/` cover the framework itself. |
| 247 | +- Config: `pytest.ini` (markers incl. `slow`, warning filters), `conftest.py` |
| 248 | + (temp cleanup, installed-vs-local switch, slow auto-marking). |
| 249 | +- `tests/datas/` holds fixtures; MT5 daily CSVs in `tests/datas/mt5_1d_data/`. |
| 250 | +- New regression tests should pass on **both** `dev` and `master` (bake master's |
| 251 | + output as the expected values). Some `tests/unit/brokers/*_performance` tests |
| 252 | + are flaky under heavy `-n 8` parallelism (timing-sensitive; pass in isolation). |
| 253 | + |
| 254 | +## Common tasks |
| 255 | + |
| 256 | +### Add an indicator |
| 257 | + |
| 258 | +1. New file in `backtrader/indicators/`; subclass `bt.Indicator`. |
| 259 | +2. `lines = ('out',)`, `params = (('period', 30),)`. |
| 260 | +3. Build the calculation in `__init__` (assign `self.lines.out = ...`) and/or |
| 261 | + implement `next()` / `once(start, end)` for explicit modes. |
| 262 | +4. Register in `indicators/__init__.py`. |
| 263 | +5. If it consumes a secondary feed or a `LinesOperation`, test runonce vs |
| 264 | + runnext parity (multi-data clock alignment). |
| 265 | + |
| 266 | +### Add a strategy |
| 267 | + |
| 268 | +1. Subclass `bt.Strategy`; declare `params`. |
| 269 | +2. Build indicators in `__init__`; trading logic in `next()`. |
| 270 | +3. Use `self.buy()/sell()/close()`. |
| 271 | + |
| 272 | +### Debug line/indicator issues |
| 273 | + |
| 274 | +- `len(obj)`, `obj._minperiod`, `obj._owner`, `obj._ltype == 0` (IndType). |
| 275 | +- Confirm `obj in owner._lineiterators[0]`. |
| 276 | +- For multi-data drift, inspect `obj._clock` and `obj._resolved_secondary_clock` |
| 277 | + and compare runonce vs runnext output (the branch-compare harness in |
| 278 | + `studies/branch_compare/` + `scripts/run_strategy_branch_compare.py` with |
| 279 | + `TradeLogger` is the established way to localize divergences). |
| 280 | + |
| 281 | +## Code style & constraints |
| 282 | + |
| 283 | +- Line length 100 (black); ruff/isort at 121. Type hints encouraged. |
| 284 | +- Bilingual (EN/ZH) comments are normal in this codebase. |
| 285 | +- **Never introduce new metaclasses** — use mixins with the `donew()` pattern. |
| 286 | +- Preserve public API compatibility. |
| 287 | +- Minimize `isinstance()`/`hasattr()`/`len()` in hot paths. |
| 288 | +- Performance work already done: metaclass removal, broker |
| 289 | + `__getattribute__`/param-cache optimization, indicator `once()` tuning. |
| 290 | + |
| 291 | +### Config files |
| 292 | + |
| 293 | +- `pyproject.toml` — black, ruff, isort, mypy, bandit, coverage. |
| 294 | +- `pytest.ini` — discovery, markers, warning filters. |
| 295 | +- `.kiro/steering/{product,tech,structure}.md` — project conventions |
| 296 | + (authoritative for build/test/structure norms). |
0 commit comments