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.
- 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.0 → backtestingfx-0.2.0}/Cargo.lock +113 -42
- {backtestingfx-0.1.0 → backtestingfx-0.2.0}/Cargo.toml +2 -1
- {backtestingfx-0.1.0 → backtestingfx-0.2.0}/PKG-INFO +96 -1
- {backtestingfx-0.1.0 → backtestingfx-0.2.0}/README.md +92 -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/compare_bt.py +90 -0
- backtestingfx-0.2.0/examples/html_report.py +61 -0
- backtestingfx-0.2.0/examples/optimize.py +58 -0
- {backtestingfx-0.1.0 → backtestingfx-0.2.0}/examples/simple_strategy.rs +8 -12
- backtestingfx-0.2.0/examples/sma_cross.py +36 -0
- {backtestingfx-0.1.0 → backtestingfx-0.2.0}/pyproject.toml +8 -2
- backtestingfx-0.2.0/src/broker.rs +454 -0
- backtestingfx-0.2.0/src/data.rs +28 -0
- {backtestingfx-0.1.0 → backtestingfx-0.2.0}/src/engine.rs +74 -6
- {backtestingfx-0.1.0 → backtestingfx-0.2.0}/src/lib.rs +4 -0
- backtestingfx-0.2.0/src/optimise.rs +166 -0
- {backtestingfx-0.1.0 → backtestingfx-0.2.0}/src/stats.rs +31 -2
- {backtestingfx-0.1.0 → backtestingfx-0.2.0}/src/strategy.rs +1 -2
- {backtestingfx-0.1.0 → backtestingfx-0.2.0}/src/types.rs +29 -0
- backtestingfx-0.2.0/tests/test_backtest.py +180 -0
- backtestingfx-0.1.0/.gitignore +0 -6
- backtestingfx-0.1.0/backtestingfx/__init__.py +0 -2
- backtestingfx-0.1.0/backtestingfx/backtest.py +0 -100
- backtestingfx-0.1.0/python/backtest.py +0 -100
- backtestingfx-0.1.0/python/simple_strategy.py +0 -28
- backtestingfx-0.1.0/src/broker.rs +0 -234
- backtestingfx-0.1.0/src/data.rs +0 -31
- {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,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,9 +19,10 @@ 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
|
+
"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.
|
|
37
|
+
version = "1.4.4"
|
|
37
38
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
38
|
-
checksum = "
|
|
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.
|
|
93
|
+
version = "0.1.11"
|
|
72
94
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
73
|
-
checksum = "
|
|
95
|
+
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
|
|
74
96
|
|
|
75
97
|
[[package]]
|
|
76
98
|
name = "futures-core"
|
|
77
|
-
version = "0.3.
|
|
99
|
+
version = "0.3.34"
|
|
78
100
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
79
|
-
checksum = "
|
|
101
|
+
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
|
80
102
|
|
|
81
103
|
[[package]]
|
|
82
104
|
name = "futures-task"
|
|
83
|
-
version = "0.3.
|
|
105
|
+
version = "0.3.34"
|
|
84
106
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
85
|
-
checksum = "
|
|
107
|
+
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
|
86
108
|
|
|
87
109
|
[[package]]
|
|
88
110
|
name = "futures-util"
|
|
89
|
-
version = "0.3.
|
|
111
|
+
version = "0.3.34"
|
|
90
112
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
91
|
-
checksum = "
|
|
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.
|
|
159
|
+
version = "0.3.104"
|
|
132
160
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
133
|
-
checksum = "
|
|
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.
|
|
170
|
+
version = "0.2.189"
|
|
143
171
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
144
|
-
checksum = "
|
|
172
|
+
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
|
145
173
|
|
|
146
174
|
[[package]]
|
|
147
175
|
name = "log"
|
|
148
|
-
version = "0.4.
|
|
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 = "
|
|
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.
|
|
209
|
+
version = "1.15.0"
|
|
176
210
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
177
|
-
checksum = "
|
|
211
|
+
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
|
178
212
|
|
|
179
213
|
[[package]]
|
|
180
214
|
name = "proc-macro2"
|
|
181
|
-
version = "1.0.
|
|
215
|
+
version = "1.0.107"
|
|
182
216
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
183
|
-
checksum = "
|
|
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.
|
|
282
|
+
version = "1.0.47"
|
|
249
283
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
250
|
-
checksum = "
|
|
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.
|
|
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 = "
|
|
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.
|
|
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 = "
|
|
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.
|
|
369
|
+
version = "0.2.127"
|
|
299
370
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
300
|
-
checksum = "
|
|
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.
|
|
382
|
+
version = "0.2.127"
|
|
312
383
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
313
|
-
checksum = "
|
|
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.
|
|
392
|
+
version = "0.2.127"
|
|
322
393
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
323
|
-
checksum = "
|
|
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.
|
|
405
|
+
version = "0.2.127"
|
|
335
406
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
336
|
-
checksum = "
|
|
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.
|
|
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.
|
|
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
|
+
[](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
|
|