backtestingfx 0.1.1__tar.gz → 0.2.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 (37) hide show
  1. backtestingfx-0.2.0/.github/workflows/ci.yml +24 -0
  2. backtestingfx-0.2.0/.github/workflows/release.yml +119 -0
  3. backtestingfx-0.2.0/.gitignore +12 -0
  4. backtestingfx-0.2.0/CHANGELOG.md +64 -0
  5. {backtestingfx-0.1.1 → backtestingfx-0.2.0}/Cargo.lock +60 -49
  6. {backtestingfx-0.1.1 → backtestingfx-0.2.0}/Cargo.toml +1 -1
  7. {backtestingfx-0.1.1 → backtestingfx-0.2.0}/PKG-INFO +94 -1
  8. {backtestingfx-0.1.1 → backtestingfx-0.2.0}/README.md +90 -0
  9. backtestingfx-0.2.0/backtestingfx/__init__.py +2 -0
  10. backtestingfx-0.2.0/backtestingfx/backtest.py +240 -0
  11. backtestingfx-0.2.0/backtestingfx/plotting.py +369 -0
  12. backtestingfx-0.2.0/data/EURUSD_1H.csv +2001 -0
  13. backtestingfx-0.2.0/examples/benchmark.py +146 -0
  14. backtestingfx-0.2.0/examples/html_report.py +61 -0
  15. backtestingfx-0.2.0/examples/optimize.py +58 -0
  16. {backtestingfx-0.1.1 → backtestingfx-0.2.0}/pyproject.toml +8 -2
  17. backtestingfx-0.2.0/src/broker.rs +454 -0
  18. backtestingfx-0.2.0/src/engine.rs +163 -0
  19. {backtestingfx-0.1.1 → backtestingfx-0.2.0}/src/lib.rs +3 -0
  20. backtestingfx-0.2.0/src/optimise.rs +166 -0
  21. {backtestingfx-0.1.1 → backtestingfx-0.2.0}/src/stats.rs +7 -0
  22. {backtestingfx-0.1.1 → backtestingfx-0.2.0}/src/strategy.rs +1 -2
  23. {backtestingfx-0.1.1 → backtestingfx-0.2.0}/src/types.rs +8 -0
  24. backtestingfx-0.2.0/tests/test_backtest.py +180 -0
  25. backtestingfx-0.1.1/.gitignore +0 -7
  26. backtestingfx-0.1.1/CHANGELOG.md +0 -30
  27. backtestingfx-0.1.1/backtestingfx/__init__.py +0 -2
  28. backtestingfx-0.1.1/backtestingfx/backtest.py +0 -129
  29. backtestingfx-0.1.1/python/backtest.py +0 -100
  30. backtestingfx-0.1.1/python/simple_strategy.py +0 -28
  31. backtestingfx-0.1.1/src/broker.rs +0 -248
  32. backtestingfx-0.1.1/src/engine.rs +0 -90
  33. {backtestingfx-0.1.1 → backtestingfx-0.2.0}/LICENSE +0 -0
  34. {backtestingfx-0.1.1 → backtestingfx-0.2.0}/examples/compare_bt.py +0 -0
  35. {backtestingfx-0.1.1 → backtestingfx-0.2.0}/examples/simple_strategy.rs +0 -0
  36. {backtestingfx-0.1.1 → backtestingfx-0.2.0}/examples/sma_cross.py +0 -0
  37. {backtestingfx-0.1.1 → backtestingfx-0.2.0}/src/data.rs +0 -0
@@ -0,0 +1,24 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: dtolnay/rust-toolchain@stable
14
+ with:
15
+ components: clippy
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.12"
19
+ # clippy does everything cargo check does, so it replaces it rather than adding to it
20
+ - run: cargo clippy --all-targets -- -D warnings
21
+ - run: cargo test
22
+ - run: python -m pip install ".[report]"
23
+ - run: python -m unittest discover -s "$GITHUB_WORKSPACE/tests"
24
+ working-directory: /tmp
@@ -0,0 +1,119 @@
1
+ # Based on `maturin generate-ci github`, then trimmed:
2
+ # - runs on tags only (the generated version rebuilt every wheel on every PR)
3
+ # - dropped linux x86/armv7/s390x/ppc64le and windows x86/aarch64 targets
4
+ # Re-add a target by copying a matrix entry; nothing else needs to change.
5
+ #
6
+ # Publishing uses PyPI trusted publishing, so there is no API token to store.
7
+ # It requires a one-time publisher registered on PyPI for this repo + release.yml.
8
+ name: Release
9
+
10
+ on:
11
+ push:
12
+ tags:
13
+ - 'v*'
14
+ workflow_dispatch: # builds wheels without publishing, for a dry run
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ linux:
21
+ runs-on: ubuntu-22.04
22
+ strategy:
23
+ matrix:
24
+ target: [x86_64, aarch64]
25
+ steps:
26
+ - uses: actions/checkout@v6
27
+ - uses: actions/setup-python@v6
28
+ with:
29
+ python-version: "3.9"
30
+ - name: Build wheels
31
+ uses: PyO3/maturin-action@v1
32
+ with:
33
+ target: ${{ matrix.target }}
34
+ args: --release --out dist --find-interpreter
35
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
36
+ manylinux: auto
37
+ - uses: actions/upload-artifact@v6
38
+ with:
39
+ name: wheels-linux-${{ matrix.target }}
40
+ path: dist
41
+
42
+ macos:
43
+ runs-on: ${{ matrix.runner }}
44
+ strategy:
45
+ matrix:
46
+ include:
47
+ - runner: macos-15-intel
48
+ target: x86_64
49
+ - runner: macos-latest
50
+ target: aarch64
51
+ steps:
52
+ - uses: actions/checkout@v6
53
+ - uses: actions/setup-python@v6
54
+ with:
55
+ python-version: "3.9"
56
+ - name: Build wheels
57
+ uses: PyO3/maturin-action@v1
58
+ with:
59
+ target: ${{ matrix.target }}
60
+ args: --release --out dist --find-interpreter
61
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
62
+ - uses: actions/upload-artifact@v6
63
+ with:
64
+ name: wheels-macos-${{ matrix.target }}
65
+ path: dist
66
+
67
+ windows:
68
+ runs-on: windows-latest
69
+ steps:
70
+ - uses: actions/checkout@v6
71
+ - uses: actions/setup-python@v6
72
+ with:
73
+ python-version: "3.13"
74
+ architecture: x64
75
+ - name: Build wheels
76
+ uses: PyO3/maturin-action@v1
77
+ with:
78
+ target: x64
79
+ args: --release --out dist --find-interpreter
80
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
81
+ - uses: actions/upload-artifact@v6
82
+ with:
83
+ name: wheels-windows-x64
84
+ path: dist
85
+
86
+ sdist:
87
+ runs-on: ubuntu-latest
88
+ steps:
89
+ - uses: actions/checkout@v6
90
+ - name: Build sdist
91
+ uses: PyO3/maturin-action@v1
92
+ with:
93
+ command: sdist
94
+ args: --out dist
95
+ - uses: actions/upload-artifact@v6
96
+ with:
97
+ name: wheels-sdist
98
+ path: dist
99
+
100
+ release:
101
+ name: Release
102
+ runs-on: ubuntu-latest
103
+ needs: [linux, macos, windows, sdist]
104
+ permissions:
105
+ id-token: write # trusted publishing + signing
106
+ contents: write # upload release artifacts
107
+ attestations: write # artifact attestation
108
+ steps:
109
+ - uses: actions/download-artifact@v7
110
+ - name: Generate artifact attestation
111
+ uses: actions/attest@v4
112
+ with:
113
+ subject-path: 'wheels-*/*'
114
+ - name: Install uv
115
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
116
+ uses: astral-sh/setup-uv@v7
117
+ - name: Publish to PyPI
118
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
119
+ run: uv publish --trusted-publishing always 'wheels-*/*'
@@ -0,0 +1,12 @@
1
+ /target
2
+ Cargo.lock
3
+ *.so
4
+ __pycache__/
5
+ ### My claude md file
6
+ CLAUDE.md
7
+ data/*
8
+ !data/EURUSD_1H.csv
9
+ .env
10
+ .venv/
11
+ uv.lock
12
+ /backtestingfx-report.html
@@ -0,0 +1,64 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0] - 2026-08-30
4
+
5
+ First release to reach PyPI since 0.1.1 — the `v0.1.2` tag predated the release
6
+ workflow, so 0.1.2 was tagged but never published. Everything listed under 0.1.2
7
+ below ships here too.
8
+
9
+ ### Added
10
+ - `Strategy.update_sl(id, stop_loss)` — move the stop on an open position in place,
11
+ so a trailing stop doesn't pay spread and commission to close and reopen
12
+ - `Strategy.close_partial(id, lot_size)` — scale out of a position, leaving the
13
+ remainder open under the same id
14
+
15
+ ### Changed
16
+ - `Broker` closes positions through one internal `settle` helper instead of four
17
+ copies of the same PnL and commission arithmetic
18
+
19
+ ## [0.1.2] - 2026-08-13
20
+
21
+ ### Added
22
+ - `Backtest.optimize()` — parallel grid search over a vectorised signal function.
23
+ Simulations run on native threads with the GIL released (`src/optimise.rs`);
24
+ 157x faster than looping `run()` over the same grid.
25
+ - `strategy_class` is now optional, so `Backtest(df, cash=...)` works for optimization
26
+ - Standalone HTML reports (`Backtest.plot()`) and `examples/html_report.py`
27
+ - Three-way speed benchmark against backtesting.py (`examples/benchmark.py`)
28
+ - Sample EURUSD hourly data is now in the repo, so the examples run on a fresh clone
29
+
30
+ ### Fixed
31
+ - Data access inside `next()` is O(1) per bar instead of O(n) — ~2.6x faster
32
+
33
+ ### Changed
34
+ - `numpy` is now a direct dependency (it was already pulled in by pandas)
35
+ - CI runs `cargo clippy -- -D warnings` in place of `cargo check`
36
+
37
+ ## [0.1.1] - 2026-07-05
38
+
39
+ ### Added
40
+ - `self.data`, `self.index`, `self.cash`, `self.equity` properties on Strategy
41
+ - Sharpe ratio in Stats output (unannualized)
42
+ - `__repr__` on Position — readable output when printing positions
43
+ - DataFrame column validation with clear error message
44
+
45
+ ### Fixed
46
+ - Trade PnL in history now stores net PnL (after exit commission) — per-trade stats were slightly optimistic
47
+ - `Position` pyclass uses `from_py_object` to fix PyO3 deprecation warning
48
+ - Removed dead `AttributeError` swallow in engine.rs
49
+
50
+ ### Examples
51
+ - Added `examples/sma_cross.py` — SMA 10/50 crossover on EURUSD hourly data
52
+ - Added `examples/compare_bt.py` — side-by-side comparison against backtesting.py
53
+
54
+ ## [0.1.0] - 2026-06-21
55
+
56
+ ### Added
57
+ - Event-driven backtesting engine on OHLCV bar data
58
+ - Simulated broker with buy, sell, close_all, close_position
59
+ - Per-position stop loss and take profit
60
+ - Realistic FX lot sizing (0.01 / 0.10 / 1.00) with contract_size and quote_to_account conversion
61
+ - Full trade history with PnL per trade
62
+ - Stats: return, win rate, avg PnL, best/worst trade, profit factor, max drawdown
63
+ - Python API — inherit Strategy, run Backtest
64
+ - PyO3 Rust extension with Python wrapper
@@ -4,9 +4,9 @@ version = 4
4
4
 
5
5
  [[package]]
6
6
  name = "android_system_properties"
7
- version = "0.1.5"
7
+ version = "0.1.6"
8
8
  source = "registry+https://github.com/rust-lang/crates.io-index"
9
- checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
9
+ checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
10
10
  dependencies = [
11
11
  "libc",
12
12
  ]
@@ -19,7 +19,7 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
19
19
 
20
20
  [[package]]
21
21
  name = "backtestingfx"
22
- version = "0.1.1"
22
+ version = "0.2.0"
23
23
  dependencies = [
24
24
  "chrono",
25
25
  "csv",
@@ -34,9 +34,9 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
34
34
 
35
35
  [[package]]
36
36
  name = "cc"
37
- version = "1.2.64"
37
+ version = "1.4.4"
38
38
  source = "registry+https://github.com/rust-lang/crates.io-index"
39
- checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"
39
+ checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
40
40
  dependencies = [
41
41
  "find-msvc-tools",
42
42
  "shlex",
@@ -90,27 +90,27 @@ dependencies = [
90
90
 
91
91
  [[package]]
92
92
  name = "find-msvc-tools"
93
- version = "0.1.9"
93
+ version = "0.1.11"
94
94
  source = "registry+https://github.com/rust-lang/crates.io-index"
95
- checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
95
+ checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
96
96
 
97
97
  [[package]]
98
98
  name = "futures-core"
99
- version = "0.3.32"
99
+ version = "0.3.34"
100
100
  source = "registry+https://github.com/rust-lang/crates.io-index"
101
- checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
101
+ checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
102
102
 
103
103
  [[package]]
104
104
  name = "futures-task"
105
- version = "0.3.32"
105
+ version = "0.3.34"
106
106
  source = "registry+https://github.com/rust-lang/crates.io-index"
107
- checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
107
+ checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
108
108
 
109
109
  [[package]]
110
110
  name = "futures-util"
111
- version = "0.3.32"
111
+ version = "0.3.34"
112
112
  source = "registry+https://github.com/rust-lang/crates.io-index"
113
- checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
113
+ checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
114
114
  dependencies = [
115
115
  "futures-core",
116
116
  "futures-task",
@@ -156,9 +156,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
156
156
 
157
157
  [[package]]
158
158
  name = "js-sys"
159
- version = "0.3.102"
159
+ version = "0.3.104"
160
160
  source = "registry+https://github.com/rust-lang/crates.io-index"
161
- checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
161
+ checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
162
162
  dependencies = [
163
163
  "cfg-if",
164
164
  "futures-util",
@@ -167,21 +167,21 @@ dependencies = [
167
167
 
168
168
  [[package]]
169
169
  name = "libc"
170
- version = "0.2.186"
170
+ version = "0.2.189"
171
171
  source = "registry+https://github.com/rust-lang/crates.io-index"
172
- checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
172
+ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
173
173
 
174
174
  [[package]]
175
175
  name = "log"
176
- version = "0.4.32"
176
+ version = "0.4.34"
177
177
  source = "registry+https://github.com/rust-lang/crates.io-index"
178
- checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
178
+ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
179
179
 
180
180
  [[package]]
181
181
  name = "memchr"
182
- version = "2.8.2"
182
+ version = "2.8.3"
183
183
  source = "registry+https://github.com/rust-lang/crates.io-index"
184
- checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
184
+ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
185
185
 
186
186
  [[package]]
187
187
  name = "num-traits"
@@ -206,15 +206,15 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
206
206
 
207
207
  [[package]]
208
208
  name = "portable-atomic"
209
- version = "1.13.1"
209
+ version = "1.15.0"
210
210
  source = "registry+https://github.com/rust-lang/crates.io-index"
211
- checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
211
+ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
212
212
 
213
213
  [[package]]
214
214
  name = "proc-macro2"
215
- version = "1.0.106"
215
+ version = "1.0.107"
216
216
  source = "registry+https://github.com/rust-lang/crates.io-index"
217
- checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
217
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
218
218
  dependencies = [
219
219
  "unicode-ident",
220
220
  ]
@@ -261,7 +261,7 @@ dependencies = [
261
261
  "proc-macro2",
262
262
  "pyo3-macros-backend",
263
263
  "quote",
264
- "syn",
264
+ "syn 2.0.119",
265
265
  ]
266
266
 
267
267
  [[package]]
@@ -274,23 +274,23 @@ dependencies = [
274
274
  "proc-macro2",
275
275
  "pyo3-build-config",
276
276
  "quote",
277
- "syn",
277
+ "syn 2.0.119",
278
278
  ]
279
279
 
280
280
  [[package]]
281
281
  name = "quote"
282
- version = "1.0.45"
282
+ version = "1.0.47"
283
283
  source = "registry+https://github.com/rust-lang/crates.io-index"
284
- checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
284
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
285
285
  dependencies = [
286
286
  "proc-macro2",
287
287
  ]
288
288
 
289
289
  [[package]]
290
290
  name = "rustversion"
291
- version = "1.0.22"
291
+ version = "1.0.23"
292
292
  source = "registry+https://github.com/rust-lang/crates.io-index"
293
- checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
293
+ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
294
294
 
295
295
  [[package]]
296
296
  name = "ryu"
@@ -300,22 +300,22 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
300
300
 
301
301
  [[package]]
302
302
  name = "serde_core"
303
- version = "1.0.228"
303
+ version = "1.0.229"
304
304
  source = "registry+https://github.com/rust-lang/crates.io-index"
305
- checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
305
+ checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
306
306
  dependencies = [
307
307
  "serde_derive",
308
308
  ]
309
309
 
310
310
  [[package]]
311
311
  name = "serde_derive"
312
- version = "1.0.228"
312
+ version = "1.0.229"
313
313
  source = "registry+https://github.com/rust-lang/crates.io-index"
314
- checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
314
+ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
315
315
  dependencies = [
316
316
  "proc-macro2",
317
317
  "quote",
318
- "syn",
318
+ "syn 3.0.4",
319
319
  ]
320
320
 
321
321
  [[package]]
@@ -332,9 +332,20 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
332
332
 
333
333
  [[package]]
334
334
  name = "syn"
335
- version = "2.0.117"
335
+ version = "2.0.119"
336
+ source = "registry+https://github.com/rust-lang/crates.io-index"
337
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
338
+ dependencies = [
339
+ "proc-macro2",
340
+ "quote",
341
+ "unicode-ident",
342
+ ]
343
+
344
+ [[package]]
345
+ name = "syn"
346
+ version = "3.0.4"
336
347
  source = "registry+https://github.com/rust-lang/crates.io-index"
337
- checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
348
+ checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
338
349
  dependencies = [
339
350
  "proc-macro2",
340
351
  "quote",
@@ -355,9 +366,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
355
366
 
356
367
  [[package]]
357
368
  name = "wasm-bindgen"
358
- version = "0.2.125"
369
+ version = "0.2.127"
359
370
  source = "registry+https://github.com/rust-lang/crates.io-index"
360
- checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
371
+ checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
361
372
  dependencies = [
362
373
  "cfg-if",
363
374
  "once_cell",
@@ -368,9 +379,9 @@ dependencies = [
368
379
 
369
380
  [[package]]
370
381
  name = "wasm-bindgen-macro"
371
- version = "0.2.125"
382
+ version = "0.2.127"
372
383
  source = "registry+https://github.com/rust-lang/crates.io-index"
373
- checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
384
+ checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
374
385
  dependencies = [
375
386
  "quote",
376
387
  "wasm-bindgen-macro-support",
@@ -378,22 +389,22 @@ dependencies = [
378
389
 
379
390
  [[package]]
380
391
  name = "wasm-bindgen-macro-support"
381
- version = "0.2.125"
392
+ version = "0.2.127"
382
393
  source = "registry+https://github.com/rust-lang/crates.io-index"
383
- checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
394
+ checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
384
395
  dependencies = [
385
396
  "bumpalo",
386
397
  "proc-macro2",
387
398
  "quote",
388
- "syn",
399
+ "syn 2.0.119",
389
400
  "wasm-bindgen-shared",
390
401
  ]
391
402
 
392
403
  [[package]]
393
404
  name = "wasm-bindgen-shared"
394
- version = "0.2.125"
405
+ version = "0.2.127"
395
406
  source = "registry+https://github.com/rust-lang/crates.io-index"
396
- checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
407
+ checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
397
408
  dependencies = [
398
409
  "unicode-ident",
399
410
  ]
@@ -419,7 +430,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
419
430
  dependencies = [
420
431
  "proc-macro2",
421
432
  "quote",
422
- "syn",
433
+ "syn 2.0.119",
423
434
  ]
424
435
 
425
436
  [[package]]
@@ -430,7 +441,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
430
441
  dependencies = [
431
442
  "proc-macro2",
432
443
  "quote",
433
- "syn",
444
+ "syn 2.0.119",
434
445
  ]
435
446
 
436
447
  [[package]]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "backtestingfx"
3
- version = "0.1.1"
3
+ version = "0.2.0"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -1,7 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backtestingfx
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Requires-Dist: pandas
5
+ Requires-Dist: numpy
6
+ Requires-Dist: plotly>=5 ; extra == 'report'
7
+ Provides-Extra: report
5
8
  License-File: LICENSE
6
9
  Summary: FX backtesting library built in Rust
7
10
  License: MIT
@@ -54,6 +57,24 @@ Profit Factor: 0.94
54
57
  Max Drawdown: 3.21%
55
58
  ```
56
59
 
60
+ ### Interactive HTML report
61
+
62
+ Install the optional report dependency and generate a self-contained HTML file:
63
+
64
+ ```bash
65
+ pip install "backtestingfx[report]"
66
+ ```
67
+
68
+ ```python
69
+ bt = Backtest(df, MyCrossStrategy, cash=10000.0, spread=0.0001)
70
+ stats = bt.run()
71
+ bt.plot("strategy-report.html")
72
+ ```
73
+
74
+ The report includes candlesticks, trade entries and exits, equity, drawdown,
75
+ trade diagnostics, and a complete trade ledger. Plotly is embedded in the file,
76
+ so the report works offline without a server.
77
+
57
78
  ## Installation
58
79
 
59
80
  ```
@@ -93,6 +114,39 @@ class MyStrategy(Strategy):
93
114
  | `self.sell(lot_size, stop_loss=None, take_profit=None)` | Open a short position |
94
115
  | `self.close_all()` | Close all open positions |
95
116
  | `self.close_position(id)` | Close a specific position by ID |
117
+ | `self.close_partial(id, lot_size)` | Close part of a position, leaving the rest open |
118
+ | `self.update_sl(id, stop_loss)` | Move a position's stop loss (returns `False` if the id is gone) |
119
+
120
+ ### Trailing stops and scaling out
121
+
122
+ `update_sl` moves the stop on an open position in place, so a trailing stop costs
123
+ nothing — closing and reopening would pay spread and commission again. `close_partial`
124
+ banks part of a position and leaves the remainder running under the same id, so you can
125
+ keep trailing it.
126
+
127
+ ```python
128
+ class TrailingBreakout(Strategy):
129
+ def next(self):
130
+ if not self.positions:
131
+ self.buy(1.0, stop_loss=self._bar.close - 0.0050)
132
+ return
133
+
134
+ position = self.positions[0]
135
+
136
+ # take half off once the trade is 50 pips up
137
+ if self._bar.close > position.entry_price + 0.0050 and position.lot_size > 0.5:
138
+ self.close_partial(position.id, 0.5)
139
+
140
+ # trail the stop 50 pips behind price, never backwards
141
+ trail = self._bar.close - 0.0050
142
+ if position.stop_loss is None or trail > position.stop_loss:
143
+ self.update_sl(position.id, trail)
144
+ ```
145
+
146
+ `update_sl` does not check which way the stop moves — widening a stop is a legitimate
147
+ thing to do, so that call is yours to make (the `trail > position.stop_loss` line above).
148
+ `close_partial` with a size at or above the position just closes it outright, and ignores
149
+ zero or negative sizes.
96
150
 
97
151
  ### Bar fields
98
152
 
@@ -105,6 +159,42 @@ self._bar.volume
105
159
  self._bar.timestamp # unix timestamp (int)
106
160
  ```
107
161
 
162
+ ## Optimization
163
+
164
+ Grid-search parameters with the simulations running in parallel Rust threads:
165
+
166
+ ```python
167
+ import numpy as np
168
+ from backtestingfx import Backtest
169
+
170
+ def sma_cross(df, fast, slow):
171
+ fast_sma = df["close"].rolling(fast).mean()
172
+ slow_sma = df["close"].rolling(slow).mean()
173
+ return np.where(fast_sma > slow_sma, 0.1, 0.0) # target lots per bar
174
+
175
+ bt = Backtest(df, cash=10_000, commission=3.5)
176
+ results = bt.optimize(sma_cross, maximize="total_return_pct",
177
+ fast=range(5, 26), slow=range(30, 101, 5))
178
+
179
+ best_params, best_stats = results[0]
180
+ ```
181
+
182
+ `optimize()` returns `[(params, stats), ...]` sorted best-first by the named `Stats`
183
+ field. Re-sort it yourself to minimise something instead.
184
+
185
+ ### Why a signal function instead of `next()`
186
+
187
+ `next()` runs in Python, so every bar needs the GIL and threads can't help. A signal
188
+ function is called **once per parameter combination**, not once per bar — it returns the
189
+ target lot size for each bar (positive long, negative short, `0.0` flat), and Rust runs
190
+ every simulation natively with the GIL released. On a 315-combination grid that is
191
+ **157x faster** than looping `Backtest.run()` over the same grid.
192
+
193
+ The trade-off: a signal function can't see the broker, so path-dependent logic (trailing
194
+ stops, pyramiding, "exit after N bars") still needs `next()` and a plain loop. Indicator
195
+ warmup must come out as `0.0`, not `NaN` — a `NaN` signal is rejected rather than
196
+ silently treated as "hold".
197
+
108
198
  ## Backtest Parameters
109
199
 
110
200
  ```python
@@ -151,6 +241,9 @@ bt = Backtest(df, MyStrategy, cash=10000.0, spread=0.00015, quote_to_account=1.2
151
241
  | `worst_trade` | Worst single trade PnL in USD |
152
242
  | `profit_factor` | Gross profit / gross loss |
153
243
  | `max_drawdown_pct` | Maximum drawdown as a percentage |
244
+ | `sharpe_ratio` | Unannualized Sharpe ratio |
245
+ | `equity_curve` | Account equity from initial cash through final liquidation |
246
+ | `trades` | Completed trades with entry, exit, size, direction, and net PnL |
154
247
 
155
248
  ## Data Format
156
249