diff --git a/Cargo.toml b/Cargo.toml index d8c2166..fedc367 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,9 @@ version = "0.1.0" edition = "2024" [lib] -name = "backtestingfx" +name = "_backtestingfx" crate-type = ["cdylib", "rlib"] [dependencies] chrono = "0.4" -pyo3 = { version = "0.28", features = ["extension-module"]} \ No newline at end of file +pyo3 = { version = "0.28", features = ["extension-module"]} diff --git a/backtestingfx/__init__.py b/backtestingfx/__init__.py new file mode 100644 index 0000000..1387cc0 --- /dev/null +++ b/backtestingfx/__init__.py @@ -0,0 +1,2 @@ +from backtestingfx._backtestingfx import Bar, Engine, Stats, Broker # type: ignore +from backtestingfx.backtest import Strategy, Backtest diff --git a/backtestingfx/__pycache__/__init__.cpython-314.pyc b/backtestingfx/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..2ccd0a3 Binary files /dev/null and b/backtestingfx/__pycache__/__init__.cpython-314.pyc differ diff --git a/backtestingfx/__pycache__/backtest.cpython-314.pyc b/backtestingfx/__pycache__/backtest.cpython-314.pyc new file mode 100644 index 0000000..d7b8e74 Binary files /dev/null and b/backtestingfx/__pycache__/backtest.cpython-314.pyc differ diff --git a/backtestingfx/_backtestingfx.cpython-314-x86_64-linux-gnu.so b/backtestingfx/_backtestingfx.cpython-314-x86_64-linux-gnu.so new file mode 100755 index 0000000..30e043c Binary files /dev/null and b/backtestingfx/_backtestingfx.cpython-314-x86_64-linux-gnu.so differ diff --git a/backtestingfx/backtest.py b/backtestingfx/backtest.py new file mode 100644 index 0000000..8858586 --- /dev/null +++ b/backtestingfx/backtest.py @@ -0,0 +1,100 @@ +from typing import Any + +from backtestingfx import _backtestingfx as _rust # type: ignore +import pandas as pd + + +class Strategy: + def __init__(self): + self._bars: Any = None + self._bar: Any = None + self._broker: Any = None + + def init(self): + pass + + def next(self): + pass + + def buy(self, lot_size, stop_loss=None, take_profit=None): + self._broker.buy( + self._bar.close, lot_size, self._bar.timestamp, stop_loss, take_profit + ) + + def sell(self, lot_size, stop_loss=None, take_profit=None): + self._broker.sell( + self._bar.close, lot_size, self._bar.timestamp, stop_loss, take_profit + ) + + def close_all(self): + self._broker.close_all(self._bar.close, self._bar.timestamp) + + def close_position(self, id): + self._broker.close_position(id, self._bar.close, self._bar.timestamp) + + +class _Adapter: + def __init__(self, strategy): + self._strategy = strategy + + def init(self, bars): + self._strategy._bars = bars + self._strategy.init() + + def next(self, bar, broker): + self._strategy._bar = bar + self._strategy._broker = broker + self._strategy.next() + + +class Backtest: + def __init__( + self, + df, + strategy_class, + cash=10000.0, + commission=0.0, + spread=0.0, + contract_size=100000.0, + quote_to_account=1.0, + ): + self._df = df + self._strategy_class = strategy_class + self._cash = cash + self._commission = commission + self._spread = spread + self._contract_size = contract_size + self._quote_to_account = quote_to_account + + def _to_bars(self): + bars = [] + for idx, row in self._df.iterrows(): + if isinstance(idx, pd.Timestamp): + ts = int(idx.timestamp()) + else: + ts = int(pd.Timestamp(row["timestamp"]).timestamp()) # type: ignore + + bars.append( + _rust.Bar( # type: ignore + timestamp=ts, + open=float(row["open"]), + high=float(row["high"]), + low=float(row["low"]), + close=float(row["close"]), + volume=float(row.get("volume", 0.0)), + ) + ) + return bars + + def run(self): + bars = self._to_bars() + engine = _rust.Engine( # type: ignore + bars, + self._cash, + self._commission, + self._spread, + self._contract_size, + self._quote_to_account, + ) + strategy = self._strategy_class() + return engine.run(_Adapter(strategy)) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0bf3b95 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[project] +name = "backtestingfx" +version = "0.1.0" +description = "FX backtesting library built in Rust" +requires-python = ">=3.9" +dependencies = ["pandas"] + +[tool.maturin] +module-name = "backtestingfx._backtestingfx" +features = ["pyo3/extension-module"] diff --git a/python/__pycache__/backtest.cpython-314.pyc b/python/__pycache__/backtest.cpython-314.pyc index de0d5e9..60751d0 100644 Binary files a/python/__pycache__/backtest.cpython-314.pyc and b/python/__pycache__/backtest.cpython-314.pyc differ diff --git a/python/simple_strategy.py b/python/simple_strategy.py index b25c64a..61344f6 100644 --- a/python/simple_strategy.py +++ b/python/simple_strategy.py @@ -1,7 +1,7 @@ import os import pandas as pd -from backtest import Backtest, Strategy +from backtestingfx import Backtest, Strategy from dotenv import load_dotenv from lse import LSE @@ -17,7 +17,7 @@ df.to_csv("data/EURUSD_1H.csv", index=False) class BuyEveryBar(Strategy): def next(self): self.close_all() - self.buy(1.0) + self.buy(0.1) df = pd.read_csv("data/EURUSD_1H.csv") @@ -25,11 +25,4 @@ df = pd.read_csv("data/EURUSD_1H.csv") bt = Backtest(df, BuyEveryBar, cash=10000.0, commission=0.0, spread=0.0001) stats = bt.run() -print(f"Return: {stats.total_return_pct:.2f}%") -print(f"Trades: {stats.num_trades}") -print(f"Win Rate: {stats.win_rate_pct:.1f}%") -print(f"Avg PnL: {stats.avg_pnl:.5f}") -print(f"Best Trade: {stats.best_trade:.5f}") -print(f"Worst Trade: {stats.worst_trade:.5f}") -print(f"Max Drawdown: {stats.max_drawdown_pct:.2f}%") -print(f"Profit Factor:{stats.profit_factor:.2f}") +print(stats) diff --git a/src/lib.rs b/src/lib.rs index 021a657..1c13028 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ pub mod types; use pyo3::prelude::*; #[pymodule] +#[pyo3(name = "_backtestingfx")] fn backtestingfx(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?;