backtestingfx 0.1.0__tar.gz → 0.1.1__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.1.0 → backtestingfx-0.1.1}/.gitignore +1 -0
- backtestingfx-0.1.1/CHANGELOG.md +30 -0
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/Cargo.lock +61 -1
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/Cargo.toml +2 -1
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/PKG-INFO +3 -1
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/README.md +2 -0
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/backtestingfx/backtest.py +29 -0
- backtestingfx-0.1.1/examples/compare_bt.py +90 -0
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/examples/simple_strategy.rs +8 -12
- backtestingfx-0.1.1/examples/sma_cross.py +36 -0
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/pyproject.toml +1 -1
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/src/broker.rs +22 -8
- backtestingfx-0.1.1/src/data.rs +28 -0
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/src/engine.rs +1 -6
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/src/lib.rs +1 -0
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/src/stats.rs +24 -2
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/src/types.rs +21 -0
- backtestingfx-0.1.0/src/data.rs +0 -31
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/LICENSE +0 -0
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/backtestingfx/__init__.py +0 -0
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/python/backtest.py +0 -0
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/python/simple_strategy.py +0 -0
- {backtestingfx-0.1.0 → backtestingfx-0.1.1}/src/strategy.rs +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.1] - 2026-07-05
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `self.data`, `self.index`, `self.cash`, `self.equity` properties on Strategy
|
|
7
|
+
- Sharpe ratio in Stats output (unannualized)
|
|
8
|
+
- `__repr__` on Position — readable output when printing positions
|
|
9
|
+
- DataFrame column validation with clear error message
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Trade PnL in history now stores net PnL (after exit commission) — per-trade stats were slightly optimistic
|
|
13
|
+
- `Position` pyclass uses `from_py_object` to fix PyO3 deprecation warning
|
|
14
|
+
- Removed dead `AttributeError` swallow in engine.rs
|
|
15
|
+
|
|
16
|
+
### Examples
|
|
17
|
+
- Added `examples/sma_cross.py` — SMA 10/50 crossover on EURUSD hourly data
|
|
18
|
+
- Added `examples/compare_bt.py` — side-by-side comparison against backtesting.py
|
|
19
|
+
|
|
20
|
+
## [0.1.0] - 2026-06-21
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Event-driven backtesting engine on OHLCV bar data
|
|
24
|
+
- Simulated broker with buy, sell, close_all, close_position
|
|
25
|
+
- Per-position stop loss and take profit
|
|
26
|
+
- Realistic FX lot sizing (0.01 / 0.10 / 1.00) with contract_size and quote_to_account conversion
|
|
27
|
+
- Full trade history with PnL per trade
|
|
28
|
+
- Stats: return, win rate, avg PnL, best/worst trade, profit factor, max drawdown
|
|
29
|
+
- Python API — inherit Strategy, run Backtest
|
|
30
|
+
- PyO3 Rust extension with Python wrapper
|
|
@@ -19,9 +19,10 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
|
|
19
19
|
|
|
20
20
|
[[package]]
|
|
21
21
|
name = "backtestingfx"
|
|
22
|
-
version = "0.1.
|
|
22
|
+
version = "0.1.1"
|
|
23
23
|
dependencies = [
|
|
24
24
|
"chrono",
|
|
25
|
+
"csv",
|
|
25
26
|
"pyo3",
|
|
26
27
|
]
|
|
27
28
|
|
|
@@ -66,6 +67,27 @@ 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
93
|
version = "0.1.9"
|
|
@@ -126,6 +148,12 @@ 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
159
|
version = "0.3.102"
|
|
@@ -149,6 +177,12 @@ version = "0.4.32"
|
|
|
149
177
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
150
178
|
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
|
151
179
|
|
|
180
|
+
[[package]]
|
|
181
|
+
name = "memchr"
|
|
182
|
+
version = "2.8.2"
|
|
183
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
184
|
+
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
|
185
|
+
|
|
152
186
|
[[package]]
|
|
153
187
|
name = "num-traits"
|
|
154
188
|
version = "0.2.19"
|
|
@@ -258,6 +292,32 @@ version = "1.0.22"
|
|
|
258
292
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
259
293
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
|
260
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.228"
|
|
304
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
305
|
+
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
|
306
|
+
dependencies = [
|
|
307
|
+
"serde_derive",
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
[[package]]
|
|
311
|
+
name = "serde_derive"
|
|
312
|
+
version = "1.0.228"
|
|
313
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
314
|
+
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|
315
|
+
dependencies = [
|
|
316
|
+
"proc-macro2",
|
|
317
|
+
"quote",
|
|
318
|
+
"syn",
|
|
319
|
+
]
|
|
320
|
+
|
|
261
321
|
[[package]]
|
|
262
322
|
name = "shlex"
|
|
263
323
|
version = "2.0.1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "backtestingfx"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.1"
|
|
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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backtestingfx
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Requires-Dist: pandas
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Summary: FX backtesting library built in Rust
|
|
@@ -11,6 +11,8 @@ Project-URL: Repository, https://github.com/KhizarImran/backtestingfx
|
|
|
11
11
|
|
|
12
12
|
# backtestingfx
|
|
13
13
|
|
|
14
|
+
[](https://pypi.org/project/backtestingfx/)
|
|
15
|
+
|
|
14
16
|
A Rust-powered FX backtesting library for Python. Write your strategy in Python, let Rust handle the heavy lifting.
|
|
15
17
|
|
|
16
18
|
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.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# backtestingfx
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/backtestingfx/)
|
|
4
|
+
|
|
3
5
|
A Rust-powered FX backtesting library for Python. Write your strategy in Python, let Rust handle the heavy lifting.
|
|
4
6
|
|
|
5
7
|
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.
|
|
@@ -9,6 +9,27 @@ class Strategy:
|
|
|
9
9
|
self._bars: Any = None
|
|
10
10
|
self._bar: Any = None
|
|
11
11
|
self._broker: Any = None
|
|
12
|
+
self._index: int = 0
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def positions(self):
|
|
16
|
+
return self._broker.positions() if self._broker else []
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def data(self):
|
|
20
|
+
return self._bars[: self._index + 1] if self._bars else []
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def index(self):
|
|
24
|
+
return self._index
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def cash(self):
|
|
28
|
+
return self._broker.cash if self._broker else 0.0
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def equity(self):
|
|
32
|
+
return self._broker.equity(self._bar.close) if self._broker else 0.0
|
|
12
33
|
|
|
13
34
|
def init(self):
|
|
14
35
|
pass
|
|
@@ -36,6 +57,7 @@ class Strategy:
|
|
|
36
57
|
class _Adapter:
|
|
37
58
|
def __init__(self, strategy):
|
|
38
59
|
self._strategy = strategy
|
|
60
|
+
self._index = 0
|
|
39
61
|
|
|
40
62
|
def init(self, bars):
|
|
41
63
|
self._strategy._bars = bars
|
|
@@ -44,6 +66,8 @@ class _Adapter:
|
|
|
44
66
|
def next(self, bar, broker):
|
|
45
67
|
self._strategy._bar = bar
|
|
46
68
|
self._strategy._broker = broker
|
|
69
|
+
self._strategy._index = self._index
|
|
70
|
+
self._index += 1
|
|
47
71
|
self._strategy.next()
|
|
48
72
|
|
|
49
73
|
|
|
@@ -67,6 +91,11 @@ class Backtest:
|
|
|
67
91
|
self._quote_to_account = quote_to_account
|
|
68
92
|
|
|
69
93
|
def _to_bars(self):
|
|
94
|
+
required = {"open", "high", "low", "close"}
|
|
95
|
+
missing = required - set(self._df.columns.str.lower())
|
|
96
|
+
if missing:
|
|
97
|
+
raise ValueError(f"DataFrame missing required columns: {sorted(missing)}")
|
|
98
|
+
|
|
70
99
|
bars = []
|
|
71
100
|
for idx, row in self._df.iterrows():
|
|
72
101
|
if isinstance(idx, pd.Timestamp):
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compares our SMA crossover logic against backtesting.py using the same data,
|
|
3
|
+
same SMA periods, and zero commission/spread so only entry/exit logic is tested.
|
|
4
|
+
|
|
5
|
+
Trade count and win rate should match between both libraries.
|
|
6
|
+
Dollar PnL will differ because the sizing models are different:
|
|
7
|
+
- backtesting.py: buys fractional units based on available cash
|
|
8
|
+
- backtestingfx: fixed lot size (0.1 lots = 10,000 units)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
from backtesting import Backtest as BtBacktest, Strategy as BtStrategy
|
|
13
|
+
from backtestingfx import Backtest, Strategy
|
|
14
|
+
|
|
15
|
+
FAST = 10
|
|
16
|
+
SLOW = 50
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ── backtesting.py ────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
class SmaCrossBt(BtStrategy):
|
|
22
|
+
fast = FAST
|
|
23
|
+
slow = SLOW
|
|
24
|
+
|
|
25
|
+
def init(self):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def next(self):
|
|
29
|
+
if len(self.data.Close) < self.slow:
|
|
30
|
+
return
|
|
31
|
+
fast_sma = self.data.Close[-self.fast:].mean()
|
|
32
|
+
slow_sma = self.data.Close[-self.slow:].mean()
|
|
33
|
+
|
|
34
|
+
if not self.position:
|
|
35
|
+
if fast_sma > slow_sma:
|
|
36
|
+
self.buy()
|
|
37
|
+
else:
|
|
38
|
+
if fast_sma < slow_sma:
|
|
39
|
+
self.position.close()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── backtestingfx ─────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
class SmaCrossFx(Strategy):
|
|
45
|
+
fast = FAST
|
|
46
|
+
slow = SLOW
|
|
47
|
+
|
|
48
|
+
def next(self):
|
|
49
|
+
if self.index < self.slow:
|
|
50
|
+
return
|
|
51
|
+
closes = [b.close for b in self.data[-self.slow:]]
|
|
52
|
+
fast_sma = sum(closes[-self.fast:]) / self.fast
|
|
53
|
+
slow_sma = sum(closes) / self.slow
|
|
54
|
+
|
|
55
|
+
if not self.positions:
|
|
56
|
+
if fast_sma > slow_sma:
|
|
57
|
+
self.buy(0.1)
|
|
58
|
+
else:
|
|
59
|
+
if fast_sma < slow_sma:
|
|
60
|
+
self.close_all()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── run both ──────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
df_raw = pd.read_csv("data/EURUSD_1H.csv")
|
|
66
|
+
|
|
67
|
+
# backtesting.py needs a DatetimeIndex and capitalised column names
|
|
68
|
+
df_bt = df_raw.copy()
|
|
69
|
+
df_bt["timestamp"] = pd.to_datetime(df_bt["timestamp"], utc=True)
|
|
70
|
+
df_bt = df_bt.set_index("timestamp")
|
|
71
|
+
df_bt.index = df_bt.index.tz_localize(None)
|
|
72
|
+
df_bt = df_bt.rename(columns={"open": "Open", "high": "High", "low": "Low", "close": "Close", "volume": "Volume"})
|
|
73
|
+
|
|
74
|
+
bt_result = BtBacktest(df_bt, SmaCrossBt, cash=10_000, commission=0).run()
|
|
75
|
+
|
|
76
|
+
fx_result = Backtest(df_raw, SmaCrossFx, cash=10_000, commission=0, spread=0).run()
|
|
77
|
+
|
|
78
|
+
# ── compare ───────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
print("=" * 45)
|
|
81
|
+
print(f"{'Metric':<20} {'backtesting.py':>12} {'backtestingfx':>12}")
|
|
82
|
+
print("=" * 45)
|
|
83
|
+
print(f"{'Trades':<20} {bt_result['# Trades']:>12} {fx_result.num_trades:>12}")
|
|
84
|
+
print(f"{'Win Rate %':<20} {bt_result['Win Rate [%]']:>12.1f} {fx_result.win_rate_pct:>12.1f}")
|
|
85
|
+
print(f"{'Return %':<20} {bt_result['Return [%]']:>12.2f} {fx_result.total_return_pct:>12.2f}")
|
|
86
|
+
print(f"{'Max Drawdown %':<20} {bt_result['Max. Drawdown [%]']:>12.2f} {fx_result.max_drawdown_pct:>12.2f}")
|
|
87
|
+
print("=" * 45)
|
|
88
|
+
print()
|
|
89
|
+
print("Note: Return % differs because backtesting.py sizes by available cash,")
|
|
90
|
+
print(" backtestingfx uses fixed 0.1 lots. Trade count + win rate should match.")
|
|
@@ -1,27 +1,23 @@
|
|
|
1
|
-
use
|
|
2
|
-
use
|
|
3
|
-
use
|
|
4
|
-
use
|
|
5
|
-
use
|
|
6
|
-
|
|
1
|
+
use _backtestingfx::broker::Broker;
|
|
2
|
+
use _backtestingfx::data::load_csv;
|
|
3
|
+
use _backtestingfx::engine::Engine;
|
|
4
|
+
use _backtestingfx::strategy::Strategy;
|
|
5
|
+
use _backtestingfx::types::Bar;
|
|
7
6
|
|
|
8
7
|
struct BuyEveryBar;
|
|
9
8
|
|
|
10
9
|
impl Strategy for BuyEveryBar {
|
|
11
|
-
fn next
|
|
10
|
+
fn next(&mut self, bar: &Bar, broker: &mut Broker) {
|
|
12
11
|
broker.close_all(bar.close, bar.timestamp); //closes any open positions
|
|
13
12
|
broker.buy(bar.close, 1.0, bar.timestamp, None, None); // can be more complicated with buy, sells, close position, close all etc.
|
|
14
13
|
}
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
fn main() {
|
|
18
|
-
let data = load_csv("
|
|
19
|
-
let mut engine = Engine::new(data, 10_000.0, 0.0, 0.00010);
|
|
17
|
+
let data = load_csv("data/EURUSD_1H.csv");
|
|
18
|
+
let mut engine = Engine::new(data, 10_000.0, 0.0, 0.00010, 1.0, 1.0);
|
|
20
19
|
let mut strategy = BuyEveryBar;
|
|
21
20
|
|
|
22
21
|
let stats = engine.run(&mut strategy);
|
|
23
22
|
println!("{}", stats);
|
|
24
23
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from backtestingfx import Backtest, Strategy
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SmaCross(Strategy):
|
|
6
|
+
fast = 10
|
|
7
|
+
slow = 50
|
|
8
|
+
|
|
9
|
+
def next(self):
|
|
10
|
+
if self.index < self.slow:
|
|
11
|
+
return
|
|
12
|
+
|
|
13
|
+
closes = [b.close for b in self.data[-self.slow :]]
|
|
14
|
+
fast_sma = sum(closes[-self.fast :]) / self.fast
|
|
15
|
+
slow_sma = sum(closes) / self.slow
|
|
16
|
+
|
|
17
|
+
if not self.positions:
|
|
18
|
+
if fast_sma > slow_sma:
|
|
19
|
+
self.buy(0.1)
|
|
20
|
+
else:
|
|
21
|
+
if fast_sma < slow_sma:
|
|
22
|
+
self.close_all()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
df = pd.read_csv("data/EURUSD_1H.csv")
|
|
26
|
+
|
|
27
|
+
stats = Backtest(
|
|
28
|
+
df,
|
|
29
|
+
SmaCross,
|
|
30
|
+
cash=10_000,
|
|
31
|
+
commission=3.5,
|
|
32
|
+
spread=0.00002,
|
|
33
|
+
contract_size=100_000,
|
|
34
|
+
).run()
|
|
35
|
+
|
|
36
|
+
print(stats)
|
|
@@ -3,8 +3,10 @@ use pyo3::prelude::*;
|
|
|
3
3
|
|
|
4
4
|
#[pyclass]
|
|
5
5
|
pub struct Broker {
|
|
6
|
+
#[pyo3(get)]
|
|
6
7
|
pub cash: f64,
|
|
7
8
|
pub initial_cash: f64,
|
|
9
|
+
next_id: u64,
|
|
8
10
|
pub positions: Vec<Position>,
|
|
9
11
|
pub trade_history: Vec<Trade>,
|
|
10
12
|
pub commission: f64,
|
|
@@ -62,13 +64,14 @@ impl Broker {
|
|
|
62
64
|
* self.contract_size
|
|
63
65
|
* self.quote_to_account
|
|
64
66
|
};
|
|
65
|
-
self.
|
|
67
|
+
let net_pnl = pnl - self.commission * position.lot_size;
|
|
68
|
+
self.cash += net_pnl;
|
|
66
69
|
self.trade_history.push(Trade {
|
|
67
70
|
entry_price: position.entry_price,
|
|
68
71
|
exit_price: close_price,
|
|
69
72
|
lot_size: position.lot_size,
|
|
70
73
|
is_long: position.is_long,
|
|
71
|
-
pnl,
|
|
74
|
+
pnl: net_pnl,
|
|
72
75
|
entry_timestamp: position.entry_timestamp,
|
|
73
76
|
exit_timestamp: bar.timestamp,
|
|
74
77
|
});
|
|
@@ -92,6 +95,7 @@ impl Broker {
|
|
|
92
95
|
Broker {
|
|
93
96
|
cash: initial_cash,
|
|
94
97
|
initial_cash,
|
|
98
|
+
next_id: 0,
|
|
95
99
|
positions: Vec::new(),
|
|
96
100
|
trade_history: Vec::new(),
|
|
97
101
|
commission,
|
|
@@ -101,6 +105,10 @@ impl Broker {
|
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
pub fn positions(&self) -> Vec<Position> {
|
|
109
|
+
self.positions.clone()
|
|
110
|
+
}
|
|
111
|
+
|
|
104
112
|
pub fn buy(
|
|
105
113
|
&mut self,
|
|
106
114
|
price: f64,
|
|
@@ -111,8 +119,10 @@ impl Broker {
|
|
|
111
119
|
) {
|
|
112
120
|
let fill_price = price + self.spread;
|
|
113
121
|
self.cash -= self.commission * lot_size;
|
|
122
|
+
let id = self.next_id;
|
|
123
|
+
self.next_id += 1;
|
|
114
124
|
self.positions.push(Position {
|
|
115
|
-
id
|
|
125
|
+
id,
|
|
116
126
|
entry_price: fill_price,
|
|
117
127
|
lot_size,
|
|
118
128
|
is_long: true,
|
|
@@ -132,8 +142,10 @@ impl Broker {
|
|
|
132
142
|
) {
|
|
133
143
|
let fill_price = price - self.spread;
|
|
134
144
|
self.cash -= self.commission * lot_size;
|
|
145
|
+
let id = self.next_id;
|
|
146
|
+
self.next_id += 1;
|
|
135
147
|
self.positions.push(Position {
|
|
136
|
-
id
|
|
148
|
+
id,
|
|
137
149
|
entry_price: fill_price,
|
|
138
150
|
lot_size,
|
|
139
151
|
is_long: false,
|
|
@@ -165,12 +177,13 @@ impl Broker {
|
|
|
165
177
|
* self.quote_to_account
|
|
166
178
|
};
|
|
167
179
|
|
|
168
|
-
self.
|
|
180
|
+
let net_pnl = pnl - self.commission * position.lot_size;
|
|
181
|
+
self.cash += net_pnl;
|
|
169
182
|
self.trade_history.push(Trade {
|
|
170
183
|
entry_price: position.entry_price,
|
|
171
184
|
lot_size: position.lot_size,
|
|
172
185
|
is_long: position.is_long,
|
|
173
|
-
pnl,
|
|
186
|
+
pnl: net_pnl,
|
|
174
187
|
entry_timestamp: position.entry_timestamp,
|
|
175
188
|
exit_timestamp: timestamp,
|
|
176
189
|
exit_price: close_price,
|
|
@@ -197,12 +210,13 @@ impl Broker {
|
|
|
197
210
|
* self.contract_size
|
|
198
211
|
* self.quote_to_account
|
|
199
212
|
};
|
|
200
|
-
self.
|
|
213
|
+
let net_pnl = pnl - self.commission * position.lot_size;
|
|
214
|
+
self.cash += net_pnl;
|
|
201
215
|
self.trade_history.push(Trade {
|
|
202
216
|
entry_price: position.entry_price,
|
|
203
217
|
lot_size: position.lot_size,
|
|
204
218
|
is_long: position.is_long,
|
|
205
|
-
pnl,
|
|
219
|
+
pnl: net_pnl,
|
|
206
220
|
entry_timestamp: position.entry_timestamp,
|
|
207
221
|
exit_timestamp: timestamp,
|
|
208
222
|
exit_price: close_price,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
use crate::types::Bar;
|
|
2
|
+
use chrono::DateTime;
|
|
3
|
+
use csv::Reader;
|
|
4
|
+
use std::fs::File;
|
|
5
|
+
|
|
6
|
+
pub fn load_csv(path: &str) -> Vec<Bar> {
|
|
7
|
+
let file = File::open(path).expect("Could not open file");
|
|
8
|
+
let mut rdr = Reader::from_reader(file);
|
|
9
|
+
let headers = rdr.headers().expect("could not read headers").clone();
|
|
10
|
+
let col = |name: &str| headers.iter().position(|h| h == name).expect(name);
|
|
11
|
+
let mut bars = Vec::new();
|
|
12
|
+
|
|
13
|
+
for result in rdr.records() {
|
|
14
|
+
let r = result.expect("could not read record");
|
|
15
|
+
let dt = DateTime::parse_from_rfc3339(&r[0]).expect("Bad timestamp");
|
|
16
|
+
|
|
17
|
+
bars.push(Bar {
|
|
18
|
+
timestamp: dt.timestamp(),
|
|
19
|
+
open: r[col("open")].parse().expect("Bad open"),
|
|
20
|
+
high: r[col("high")].parse().expect("Bad high"),
|
|
21
|
+
low: r[col("low")].parse().expect("Bad low"),
|
|
22
|
+
close: r[col("close")].parse().expect("Bad close"),
|
|
23
|
+
volume: r[col("volume")].parse().expect("Bad volume"),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
bars
|
|
28
|
+
}
|
|
@@ -54,12 +54,7 @@ impl Engine {
|
|
|
54
54
|
pub fn run_py(&mut self, py: Python<'_>, strategy: Py<PyAny>) -> PyResult<Stats> {
|
|
55
55
|
self.equity_curve.clear();
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
if let Err(e) = init_result {
|
|
59
|
-
if !e.is_instance_of::<pyo3::exceptions::PyAttributeError>(py) {
|
|
60
|
-
return Err(e);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
57
|
+
strategy.bind(py).call_method1("init", (self.data.clone(),))?;
|
|
63
58
|
|
|
64
59
|
let broker_py = Py::new(
|
|
65
60
|
py,
|
|
@@ -25,6 +25,24 @@ pub struct Stats {
|
|
|
25
25
|
pub profit_factor: f64,
|
|
26
26
|
#[pyo3(get)]
|
|
27
27
|
pub max_drawdown_pct: f64,
|
|
28
|
+
#[pyo3(get)]
|
|
29
|
+
pub sharpe_ratio: f64,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn sharpe_ratio(equity_curve: &[f64]) -> f64 {
|
|
33
|
+
if equity_curve.len() < 2 {
|
|
34
|
+
return 0.0;
|
|
35
|
+
}
|
|
36
|
+
let returns: Vec<f64> = equity_curve
|
|
37
|
+
.windows(2)
|
|
38
|
+
.map(|w| (w[1] - w[0]) / w[0])
|
|
39
|
+
.collect();
|
|
40
|
+
let mean = returns.iter().sum::<f64>() / returns.len() as f64;
|
|
41
|
+
let variance = returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / returns.len() as f64;
|
|
42
|
+
if variance == 0.0 {
|
|
43
|
+
return 0.0;
|
|
44
|
+
}
|
|
45
|
+
mean / variance.sqrt()
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
fn max_drawdown(equity_curve: &[f64]) -> f64 {
|
|
@@ -101,6 +119,7 @@ impl Stats {
|
|
|
101
119
|
f64::INFINITY
|
|
102
120
|
};
|
|
103
121
|
let max_drawdown_pct = max_drawdown(equity_curve);
|
|
122
|
+
let sharpe_ratio = sharpe_ratio(equity_curve);
|
|
104
123
|
|
|
105
124
|
Stats {
|
|
106
125
|
initial_cash,
|
|
@@ -114,6 +133,7 @@ impl Stats {
|
|
|
114
133
|
worst_trade: if num_trades > 0 { worst_trade } else { 0.0 },
|
|
115
134
|
profit_factor,
|
|
116
135
|
max_drawdown_pct,
|
|
136
|
+
sharpe_ratio,
|
|
117
137
|
}
|
|
118
138
|
}
|
|
119
139
|
}
|
|
@@ -132,7 +152,8 @@ impl std::fmt::Display for Stats {
|
|
|
132
152
|
Best Trade: {:.5}\n\
|
|
133
153
|
Worst Trade: {:.5}\n\
|
|
134
154
|
Profit Factor: {:.2}\n\
|
|
135
|
-
Max Drawdown: {:.2}
|
|
155
|
+
Max Drawdown: {:.2}%\n\
|
|
156
|
+
Sharpe Ratio: {:.4} (unannualized)",
|
|
136
157
|
self.initial_cash,
|
|
137
158
|
self.final_cash,
|
|
138
159
|
self.total_return_pct,
|
|
@@ -142,7 +163,8 @@ impl std::fmt::Display for Stats {
|
|
|
142
163
|
self.best_trade,
|
|
143
164
|
self.worst_trade,
|
|
144
165
|
self.profit_factor,
|
|
145
|
-
self.max_drawdown_pct
|
|
166
|
+
self.max_drawdown_pct,
|
|
167
|
+
self.sharpe_ratio
|
|
146
168
|
)
|
|
147
169
|
}
|
|
148
170
|
}
|
|
@@ -33,18 +33,39 @@ impl Bar {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
#[pyclass(from_py_object)]
|
|
36
37
|
#[derive(Debug, Clone)]
|
|
37
38
|
pub struct Position {
|
|
38
39
|
// this is for the trading position
|
|
40
|
+
#[pyo3(get)]
|
|
39
41
|
pub id: u64,
|
|
42
|
+
#[pyo3(get)]
|
|
40
43
|
pub entry_price: f64,
|
|
44
|
+
#[pyo3(get)]
|
|
41
45
|
pub lot_size: f64,
|
|
46
|
+
#[pyo3(get)]
|
|
42
47
|
pub is_long: bool,
|
|
48
|
+
#[pyo3(get)]
|
|
43
49
|
pub entry_timestamp: i64,
|
|
50
|
+
#[pyo3(get)]
|
|
44
51
|
pub stop_loss: Option<f64>,
|
|
52
|
+
#[pyo3(get)]
|
|
45
53
|
pub take_profit: Option<f64>,
|
|
46
54
|
}
|
|
47
55
|
|
|
56
|
+
#[pymethods]
|
|
57
|
+
impl Position {
|
|
58
|
+
fn __repr__(&self) -> String {
|
|
59
|
+
format!(
|
|
60
|
+
"Position(id={}, {} {:.2} lots @ {:.5})",
|
|
61
|
+
self.id,
|
|
62
|
+
if self.is_long { "LONG" } else { "SHORT" },
|
|
63
|
+
self.lot_size,
|
|
64
|
+
self.entry_price,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
48
69
|
#[derive(Debug, Clone)]
|
|
49
70
|
pub struct Trade {
|
|
50
71
|
// the actual trade
|
backtestingfx-0.1.0/src/data.rs
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
use std::fs::File;
|
|
2
|
-
use std::io::{BufRead, BufReader};
|
|
3
|
-
use crate::types::Bar;
|
|
4
|
-
use chrono::DateTime;
|
|
5
|
-
|
|
6
|
-
pub fn load_csv(path: &str) -> Vec<Bar> {
|
|
7
|
-
let file = File::open(path).expect("Could not open file");
|
|
8
|
-
let reader = BufReader::new(file);
|
|
9
|
-
let mut bars = Vec::new();
|
|
10
|
-
|
|
11
|
-
for line in reader.lines().skip(1) { //skips the header row
|
|
12
|
-
let line = line.expect("could not read line");
|
|
13
|
-
let cols: Vec<&str> = line.split(',').collect();
|
|
14
|
-
|
|
15
|
-
let dt = DateTime::parse_from_rfc3339(cols[0]).expect("Bad Timestamp"); // this handles the timestamp to be in 2024-01-01 00:00:00 kind of way
|
|
16
|
-
|
|
17
|
-
let bar = Bar {
|
|
18
|
-
timestamp: dt.timestamp(),
|
|
19
|
-
open: cols[1].parse().expect("Bad open"),
|
|
20
|
-
high: cols[2].parse().expect("Bad high"),
|
|
21
|
-
low: cols[3].parse().expect("Bad low"),
|
|
22
|
-
close: cols[4].parse().expect("Bad close"),
|
|
23
|
-
volume: cols[5].parse().expect("Bad volume"),
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
bars.push(bar); // pushes each bar from the csv to an element in Bar
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
bars
|
|
31
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|