diff --git a/.gitignore b/.gitignore index 8ce2613..9e88893 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /target Cargo.lock -### My claude md file +### My claude md file CLAUDE.md -*/data/ +data/ +.env diff --git a/python/__pycache__/backtest.cpython-314.pyc b/python/__pycache__/backtest.cpython-314.pyc new file mode 100644 index 0000000..de0d5e9 Binary files /dev/null and b/python/__pycache__/backtest.cpython-314.pyc differ diff --git a/python/backtest.py b/python/backtest.py new file mode 100644 index 0000000..33015d0 --- /dev/null +++ b/python/backtest.py @@ -0,0 +1,69 @@ +import backtestingfx as _rust +import pandas as pd + + +class Strategy: + 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): + self._df = df + self._strategy_class = strategy_class + self._cash = cash + self._commission = commission + self._spread = spread + + 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()) + + bars.append(_rust.Bar( + 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(bars, self._cash, self._commission, self._spread) + strategy = self._strategy_class() + return engine.run(_Adapter(strategy)) diff --git a/python/simple_strategy.py b/python/simple_strategy.py new file mode 100644 index 0000000..b25c64a --- /dev/null +++ b/python/simple_strategy.py @@ -0,0 +1,35 @@ +import os + +import pandas as pd +from backtest import Backtest, Strategy +from dotenv import load_dotenv +from lse import LSE + +load_dotenv() + +client = LSE(api_key=os.environ["LSE_API_KEY"]) + +rows = client.candles("EUR/USD", "1h", limit=2000) +df = pd.DataFrame(rows) +df.to_csv("data/EURUSD_1H.csv", index=False) + + +class BuyEveryBar(Strategy): + def next(self): + self.close_all() + self.buy(1.0) + + +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}")