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.
- backtestingfx-0.2.0/.github/workflows/ci.yml +24 -0
- backtestingfx-0.2.0/.github/workflows/release.yml +119 -0
- backtestingfx-0.2.0/.gitignore +12 -0
- backtestingfx-0.2.0/CHANGELOG.md +64 -0
- {backtestingfx-0.1.1 → backtestingfx-0.2.0}/Cargo.lock +60 -49
- {backtestingfx-0.1.1 → backtestingfx-0.2.0}/Cargo.toml +1 -1
- {backtestingfx-0.1.1 → backtestingfx-0.2.0}/PKG-INFO +94 -1
- {backtestingfx-0.1.1 → backtestingfx-0.2.0}/README.md +90 -0
- backtestingfx-0.2.0/backtestingfx/__init__.py +2 -0
- backtestingfx-0.2.0/backtestingfx/backtest.py +240 -0
- backtestingfx-0.2.0/backtestingfx/plotting.py +369 -0
- backtestingfx-0.2.0/data/EURUSD_1H.csv +2001 -0
- backtestingfx-0.2.0/examples/benchmark.py +146 -0
- backtestingfx-0.2.0/examples/html_report.py +61 -0
- backtestingfx-0.2.0/examples/optimize.py +58 -0
- {backtestingfx-0.1.1 → backtestingfx-0.2.0}/pyproject.toml +8 -2
- backtestingfx-0.2.0/src/broker.rs +454 -0
- backtestingfx-0.2.0/src/engine.rs +163 -0
- {backtestingfx-0.1.1 → backtestingfx-0.2.0}/src/lib.rs +3 -0
- backtestingfx-0.2.0/src/optimise.rs +166 -0
- {backtestingfx-0.1.1 → backtestingfx-0.2.0}/src/stats.rs +7 -0
- {backtestingfx-0.1.1 → backtestingfx-0.2.0}/src/strategy.rs +1 -2
- {backtestingfx-0.1.1 → backtestingfx-0.2.0}/src/types.rs +8 -0
- backtestingfx-0.2.0/tests/test_backtest.py +180 -0
- backtestingfx-0.1.1/.gitignore +0 -7
- backtestingfx-0.1.1/CHANGELOG.md +0 -30
- backtestingfx-0.1.1/backtestingfx/__init__.py +0 -2
- backtestingfx-0.1.1/backtestingfx/backtest.py +0 -129
- backtestingfx-0.1.1/python/backtest.py +0 -100
- backtestingfx-0.1.1/python/simple_strategy.py +0 -28
- backtestingfx-0.1.1/src/broker.rs +0 -248
- backtestingfx-0.1.1/src/engine.rs +0 -90
- {backtestingfx-0.1.1 → backtestingfx-0.2.0}/LICENSE +0 -0
- {backtestingfx-0.1.1 → backtestingfx-0.2.0}/examples/compare_bt.py +0 -0
- {backtestingfx-0.1.1 → backtestingfx-0.2.0}/examples/simple_strategy.rs +0 -0
- {backtestingfx-0.1.1 → backtestingfx-0.2.0}/examples/sma_cross.py +0 -0
- {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,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.
|
|
7
|
+
version = "0.1.6"
|
|
8
8
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
-
checksum = "
|
|
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.
|
|
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.
|
|
37
|
+
version = "1.4.4"
|
|
38
38
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
39
|
-
checksum = "
|
|
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.
|
|
93
|
+
version = "0.1.11"
|
|
94
94
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
95
|
-
checksum = "
|
|
95
|
+
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
|
|
96
96
|
|
|
97
97
|
[[package]]
|
|
98
98
|
name = "futures-core"
|
|
99
|
-
version = "0.3.
|
|
99
|
+
version = "0.3.34"
|
|
100
100
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
101
|
-
checksum = "
|
|
101
|
+
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
|
102
102
|
|
|
103
103
|
[[package]]
|
|
104
104
|
name = "futures-task"
|
|
105
|
-
version = "0.3.
|
|
105
|
+
version = "0.3.34"
|
|
106
106
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
107
|
-
checksum = "
|
|
107
|
+
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
|
108
108
|
|
|
109
109
|
[[package]]
|
|
110
110
|
name = "futures-util"
|
|
111
|
-
version = "0.3.
|
|
111
|
+
version = "0.3.34"
|
|
112
112
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
113
|
-
checksum = "
|
|
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.
|
|
159
|
+
version = "0.3.104"
|
|
160
160
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
161
|
-
checksum = "
|
|
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.
|
|
170
|
+
version = "0.2.189"
|
|
171
171
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
172
|
-
checksum = "
|
|
172
|
+
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
|
173
173
|
|
|
174
174
|
[[package]]
|
|
175
175
|
name = "log"
|
|
176
|
-
version = "0.4.
|
|
176
|
+
version = "0.4.34"
|
|
177
177
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
178
|
-
checksum = "
|
|
178
|
+
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
|
179
179
|
|
|
180
180
|
[[package]]
|
|
181
181
|
name = "memchr"
|
|
182
|
-
version = "2.8.
|
|
182
|
+
version = "2.8.3"
|
|
183
183
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
184
|
-
checksum = "
|
|
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.
|
|
209
|
+
version = "1.15.0"
|
|
210
210
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
211
|
-
checksum = "
|
|
211
|
+
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
|
212
212
|
|
|
213
213
|
[[package]]
|
|
214
214
|
name = "proc-macro2"
|
|
215
|
-
version = "1.0.
|
|
215
|
+
version = "1.0.107"
|
|
216
216
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
217
|
-
checksum = "
|
|
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.
|
|
282
|
+
version = "1.0.47"
|
|
283
283
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
284
|
-
checksum = "
|
|
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.
|
|
291
|
+
version = "1.0.23"
|
|
292
292
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
293
|
-
checksum = "
|
|
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.
|
|
303
|
+
version = "1.0.229"
|
|
304
304
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
305
|
-
checksum = "
|
|
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.
|
|
312
|
+
version = "1.0.229"
|
|
313
313
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
314
|
-
checksum = "
|
|
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.
|
|
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 = "
|
|
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.
|
|
369
|
+
version = "0.2.127"
|
|
359
370
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
360
|
-
checksum = "
|
|
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.
|
|
382
|
+
version = "0.2.127"
|
|
372
383
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
373
|
-
checksum = "
|
|
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.
|
|
392
|
+
version = "0.2.127"
|
|
382
393
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
383
|
-
checksum = "
|
|
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.
|
|
405
|
+
version = "0.2.127"
|
|
395
406
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
396
|
-
checksum = "
|
|
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,7 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backtestingfx
|
|
3
|
-
Version: 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
|
|
@@ -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
|
|