backtestingfx 0.1.0__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 (36) 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.0 → backtestingfx-0.2.0}/Cargo.lock +113 -42
  6. {backtestingfx-0.1.0 → backtestingfx-0.2.0}/Cargo.toml +2 -1
  7. {backtestingfx-0.1.0 → backtestingfx-0.2.0}/PKG-INFO +96 -1
  8. {backtestingfx-0.1.0 → backtestingfx-0.2.0}/README.md +92 -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/compare_bt.py +90 -0
  15. backtestingfx-0.2.0/examples/html_report.py +61 -0
  16. backtestingfx-0.2.0/examples/optimize.py +58 -0
  17. {backtestingfx-0.1.0 → backtestingfx-0.2.0}/examples/simple_strategy.rs +8 -12
  18. backtestingfx-0.2.0/examples/sma_cross.py +36 -0
  19. {backtestingfx-0.1.0 → backtestingfx-0.2.0}/pyproject.toml +8 -2
  20. backtestingfx-0.2.0/src/broker.rs +454 -0
  21. backtestingfx-0.2.0/src/data.rs +28 -0
  22. {backtestingfx-0.1.0 → backtestingfx-0.2.0}/src/engine.rs +74 -6
  23. {backtestingfx-0.1.0 → backtestingfx-0.2.0}/src/lib.rs +4 -0
  24. backtestingfx-0.2.0/src/optimise.rs +166 -0
  25. {backtestingfx-0.1.0 → backtestingfx-0.2.0}/src/stats.rs +31 -2
  26. {backtestingfx-0.1.0 → backtestingfx-0.2.0}/src/strategy.rs +1 -2
  27. {backtestingfx-0.1.0 → backtestingfx-0.2.0}/src/types.rs +29 -0
  28. backtestingfx-0.2.0/tests/test_backtest.py +180 -0
  29. backtestingfx-0.1.0/.gitignore +0 -6
  30. backtestingfx-0.1.0/backtestingfx/__init__.py +0 -2
  31. backtestingfx-0.1.0/backtestingfx/backtest.py +0 -100
  32. backtestingfx-0.1.0/python/backtest.py +0 -100
  33. backtestingfx-0.1.0/python/simple_strategy.py +0 -28
  34. backtestingfx-0.1.0/src/broker.rs +0 -234
  35. backtestingfx-0.1.0/src/data.rs +0 -31
  36. {backtestingfx-0.1.0 → backtestingfx-0.2.0}/LICENSE +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,9 +19,10 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
19
19
 
20
20
  [[package]]
21
21
  name = "backtestingfx"
22
- version = "0.1.0"
22
+ version = "0.2.0"
23
23
  dependencies = [
24
24
  "chrono",
25
+ "csv",
25
26
  "pyo3",
26
27
  ]
27
28
 
@@ -33,9 +34,9 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
33
34
 
34
35
  [[package]]
35
36
  name = "cc"
36
- version = "1.2.64"
37
+ version = "1.4.4"
37
38
  source = "registry+https://github.com/rust-lang/crates.io-index"
38
- checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"
39
+ checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
39
40
  dependencies = [
40
41
  "find-msvc-tools",
41
42
  "shlex",
@@ -66,29 +67,50 @@ version = "0.8.7"
66
67
  source = "registry+https://github.com/rust-lang/crates.io-index"
67
68
  checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
68
69
 
70
+ [[package]]
71
+ name = "csv"
72
+ version = "1.4.0"
73
+ source = "registry+https://github.com/rust-lang/crates.io-index"
74
+ checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
75
+ dependencies = [
76
+ "csv-core",
77
+ "itoa",
78
+ "ryu",
79
+ "serde_core",
80
+ ]
81
+
82
+ [[package]]
83
+ name = "csv-core"
84
+ version = "0.1.13"
85
+ source = "registry+https://github.com/rust-lang/crates.io-index"
86
+ checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
87
+ dependencies = [
88
+ "memchr",
89
+ ]
90
+
69
91
  [[package]]
70
92
  name = "find-msvc-tools"
71
- version = "0.1.9"
93
+ version = "0.1.11"
72
94
  source = "registry+https://github.com/rust-lang/crates.io-index"
73
- checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
95
+ checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
74
96
 
75
97
  [[package]]
76
98
  name = "futures-core"
77
- version = "0.3.32"
99
+ version = "0.3.34"
78
100
  source = "registry+https://github.com/rust-lang/crates.io-index"
79
- checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
101
+ checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
80
102
 
81
103
  [[package]]
82
104
  name = "futures-task"
83
- version = "0.3.32"
105
+ version = "0.3.34"
84
106
  source = "registry+https://github.com/rust-lang/crates.io-index"
85
- checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
107
+ checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
86
108
 
87
109
  [[package]]
88
110
  name = "futures-util"
89
- version = "0.3.32"
111
+ version = "0.3.34"
90
112
  source = "registry+https://github.com/rust-lang/crates.io-index"
91
- checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
113
+ checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
92
114
  dependencies = [
93
115
  "futures-core",
94
116
  "futures-task",
@@ -126,11 +148,17 @@ dependencies = [
126
148
  "cc",
127
149
  ]
128
150
 
151
+ [[package]]
152
+ name = "itoa"
153
+ version = "1.0.18"
154
+ source = "registry+https://github.com/rust-lang/crates.io-index"
155
+ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
156
+
129
157
  [[package]]
130
158
  name = "js-sys"
131
- version = "0.3.102"
159
+ version = "0.3.104"
132
160
  source = "registry+https://github.com/rust-lang/crates.io-index"
133
- checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
161
+ checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
134
162
  dependencies = [
135
163
  "cfg-if",
136
164
  "futures-util",
@@ -139,15 +167,21 @@ dependencies = [
139
167
 
140
168
  [[package]]
141
169
  name = "libc"
142
- version = "0.2.186"
170
+ version = "0.2.189"
143
171
  source = "registry+https://github.com/rust-lang/crates.io-index"
144
- checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
172
+ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
145
173
 
146
174
  [[package]]
147
175
  name = "log"
148
- version = "0.4.32"
176
+ version = "0.4.34"
177
+ source = "registry+https://github.com/rust-lang/crates.io-index"
178
+ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
179
+
180
+ [[package]]
181
+ name = "memchr"
182
+ version = "2.8.3"
149
183
  source = "registry+https://github.com/rust-lang/crates.io-index"
150
- checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
184
+ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
151
185
 
152
186
  [[package]]
153
187
  name = "num-traits"
@@ -172,15 +206,15 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
172
206
 
173
207
  [[package]]
174
208
  name = "portable-atomic"
175
- version = "1.13.1"
209
+ version = "1.15.0"
176
210
  source = "registry+https://github.com/rust-lang/crates.io-index"
177
- checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
211
+ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
178
212
 
179
213
  [[package]]
180
214
  name = "proc-macro2"
181
- version = "1.0.106"
215
+ version = "1.0.107"
182
216
  source = "registry+https://github.com/rust-lang/crates.io-index"
183
- checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
217
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
184
218
  dependencies = [
185
219
  "unicode-ident",
186
220
  ]
@@ -227,7 +261,7 @@ dependencies = [
227
261
  "proc-macro2",
228
262
  "pyo3-macros-backend",
229
263
  "quote",
230
- "syn",
264
+ "syn 2.0.119",
231
265
  ]
232
266
 
233
267
  [[package]]
@@ -240,23 +274,49 @@ dependencies = [
240
274
  "proc-macro2",
241
275
  "pyo3-build-config",
242
276
  "quote",
243
- "syn",
277
+ "syn 2.0.119",
244
278
  ]
245
279
 
246
280
  [[package]]
247
281
  name = "quote"
248
- version = "1.0.45"
282
+ version = "1.0.47"
249
283
  source = "registry+https://github.com/rust-lang/crates.io-index"
250
- checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
284
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
251
285
  dependencies = [
252
286
  "proc-macro2",
253
287
  ]
254
288
 
255
289
  [[package]]
256
290
  name = "rustversion"
257
- version = "1.0.22"
291
+ version = "1.0.23"
292
+ source = "registry+https://github.com/rust-lang/crates.io-index"
293
+ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
294
+
295
+ [[package]]
296
+ name = "ryu"
297
+ version = "1.0.23"
298
+ source = "registry+https://github.com/rust-lang/crates.io-index"
299
+ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
300
+
301
+ [[package]]
302
+ name = "serde_core"
303
+ version = "1.0.229"
304
+ source = "registry+https://github.com/rust-lang/crates.io-index"
305
+ checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
306
+ dependencies = [
307
+ "serde_derive",
308
+ ]
309
+
310
+ [[package]]
311
+ name = "serde_derive"
312
+ version = "1.0.229"
258
313
  source = "registry+https://github.com/rust-lang/crates.io-index"
259
- checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
314
+ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
315
+ dependencies = [
316
+ "proc-macro2",
317
+ "quote",
318
+ "syn 3.0.4",
319
+ ]
260
320
 
261
321
  [[package]]
262
322
  name = "shlex"
@@ -272,9 +332,20 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
272
332
 
273
333
  [[package]]
274
334
  name = "syn"
275
- 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"
276
347
  source = "registry+https://github.com/rust-lang/crates.io-index"
277
- checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
348
+ checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
278
349
  dependencies = [
279
350
  "proc-macro2",
280
351
  "quote",
@@ -295,9 +366,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
295
366
 
296
367
  [[package]]
297
368
  name = "wasm-bindgen"
298
- version = "0.2.125"
369
+ version = "0.2.127"
299
370
  source = "registry+https://github.com/rust-lang/crates.io-index"
300
- checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
371
+ checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
301
372
  dependencies = [
302
373
  "cfg-if",
303
374
  "once_cell",
@@ -308,9 +379,9 @@ dependencies = [
308
379
 
309
380
  [[package]]
310
381
  name = "wasm-bindgen-macro"
311
- version = "0.2.125"
382
+ version = "0.2.127"
312
383
  source = "registry+https://github.com/rust-lang/crates.io-index"
313
- checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
384
+ checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
314
385
  dependencies = [
315
386
  "quote",
316
387
  "wasm-bindgen-macro-support",
@@ -318,22 +389,22 @@ dependencies = [
318
389
 
319
390
  [[package]]
320
391
  name = "wasm-bindgen-macro-support"
321
- version = "0.2.125"
392
+ version = "0.2.127"
322
393
  source = "registry+https://github.com/rust-lang/crates.io-index"
323
- checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
394
+ checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
324
395
  dependencies = [
325
396
  "bumpalo",
326
397
  "proc-macro2",
327
398
  "quote",
328
- "syn",
399
+ "syn 2.0.119",
329
400
  "wasm-bindgen-shared",
330
401
  ]
331
402
 
332
403
  [[package]]
333
404
  name = "wasm-bindgen-shared"
334
- version = "0.2.125"
405
+ version = "0.2.127"
335
406
  source = "registry+https://github.com/rust-lang/crates.io-index"
336
- checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
407
+ checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
337
408
  dependencies = [
338
409
  "unicode-ident",
339
410
  ]
@@ -359,7 +430,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
359
430
  dependencies = [
360
431
  "proc-macro2",
361
432
  "quote",
362
- "syn",
433
+ "syn 2.0.119",
363
434
  ]
364
435
 
365
436
  [[package]]
@@ -370,7 +441,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
370
441
  dependencies = [
371
442
  "proc-macro2",
372
443
  "quote",
373
- "syn",
444
+ "syn 2.0.119",
374
445
  ]
375
446
 
376
447
  [[package]]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "backtestingfx"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -11,3 +11,4 @@ crate-type = ["cdylib", "rlib"]
11
11
  [dependencies]
12
12
  chrono = "0.4"
13
13
  pyo3 = { version = "0.28", features = ["extension-module"]}
14
+ csv = "1"
@@ -1,7 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backtestingfx
3
- Version: 0.1.0
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
@@ -11,6 +14,8 @@ Project-URL: Repository, https://github.com/KhizarImran/backtestingfx
11
14
 
12
15
  # backtestingfx
13
16
 
17
+ [![PyPI](https://img.shields.io/pypi/v/backtestingfx)](https://pypi.org/project/backtestingfx/)
18
+
14
19
  A Rust-powered FX backtesting library for Python. Write your strategy in Python, let Rust handle the heavy lifting.
15
20
 
16
21
  Inspired by [backtesting.py](https://kernc.github.io/backtesting.py/) but built specifically for forex — lot sizes, pip-based PnL, stop loss, take profit, and realistic account currency conversion.
@@ -52,6 +57,24 @@ Profit Factor: 0.94
52
57
  Max Drawdown: 3.21%
53
58
  ```
54
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
+
55
78
  ## Installation
56
79
 
57
80
  ```
@@ -91,6 +114,39 @@ class MyStrategy(Strategy):
91
114
  | `self.sell(lot_size, stop_loss=None, take_profit=None)` | Open a short position |
92
115
  | `self.close_all()` | Close all open positions |
93
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.
94
150
 
95
151
  ### Bar fields
96
152
 
@@ -103,6 +159,42 @@ self._bar.volume
103
159
  self._bar.timestamp # unix timestamp (int)
104
160
  ```
105
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
+
106
198
  ## Backtest Parameters
107
199
 
108
200
  ```python
@@ -149,6 +241,9 @@ bt = Backtest(df, MyStrategy, cash=10000.0, spread=0.00015, quote_to_account=1.2
149
241
  | `worst_trade` | Worst single trade PnL in USD |
150
242
  | `profit_factor` | Gross profit / gross loss |
151
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 |
152
247
 
153
248
  ## Data Format
154
249