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.
@@ -1,5 +1,6 @@
1
1
  /target
2
2
  Cargo.lock
3
+ *.so
3
4
  ### My claude md file
4
5
  CLAUDE.md
5
6
  data/
@@ -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.0"
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.0"
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.0
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
+ [![PyPI](https://img.shields.io/pypi/v/backtestingfx)](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
+ [![PyPI](https://img.shields.io/pypi/v/backtestingfx)](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 backtestingfx::types::Bar;
2
- use backtestingfx::broker::Broker;
3
- use backtestingfx::strategy::Strategy;
4
- use backtestingfx::engine::Engine;
5
- use backtestingfx::data::load_csv;
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 (&mut self, bar: &Bar, broker: &mut Broker) {
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("examples/data/eurusd_lse_1h.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)
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "backtestingfx"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "FX backtesting library built in Rust"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = ["pandas"]
@@ -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.cash += pnl;
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: self.positions.len() as u64,
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: self.positions.len() as u64,
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.cash += pnl;
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.cash += pnl;
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
- let init_result = strategy.bind(py).call_method1("init", (self.data.clone(),));
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,
@@ -14,5 +14,6 @@ fn backtestingfx(m: &Bound<'_, PyModule>) -> PyResult<()> {
14
14
  m.add_class::<stats::Stats>()?;
15
15
  m.add_class::<broker::Broker>()?;
16
16
  m.add_class::<engine::Engine>()?;
17
+ m.add_class::<types::Position>()?;
17
18
  Ok(())
18
19
  }
@@ -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
@@ -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