diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b07b295 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## [0.1.0] - 2026-06-21 + +### Added +- Event-driven backtesting engine on OHLCV bar data +- Simulated broker with buy, sell, close_all, close_position +- Per-position stop loss and take profit +- Realistic FX lot sizing (0.01 / 0.10 / 1.00) with contract_size and quote_to_account conversion +- Full trade history with PnL per trade +- Stats: return, win rate, avg PnL, best/worst trade, profit factor, max drawdown +- Python API — inherit Strategy, run Backtest +- PyO3 Rust extension with Python wrapper diff --git a/Cargo.toml b/Cargo.toml index fedc367..60b855e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ crate-type = ["cdylib", "rlib"] [dependencies] chrono = "0.4" pyo3 = { version = "0.28", features = ["extension-module"]} +csv = "1" diff --git a/examples/simple_strategy.rs b/examples/simple_strategy.rs index c63426e..7cc1304 100644 --- a/examples/simple_strategy.rs +++ b/examples/simple_strategy.rs @@ -1,27 +1,23 @@ -use backtestingfx::types::Bar; -use backtestingfx::broker::Broker; -use backtestingfx::strategy::Strategy; -use backtestingfx::engine::Engine; -use backtestingfx::data::load_csv; - +use _backtestingfx::broker::Broker; +use _backtestingfx::data::load_csv; +use _backtestingfx::engine::Engine; +use _backtestingfx::strategy::Strategy; +use _backtestingfx::types::Bar; struct BuyEveryBar; impl Strategy for BuyEveryBar { - fn next (&mut self, bar: &Bar, broker: &mut Broker) { + fn next(&mut self, bar: &Bar, broker: &mut Broker) { broker.close_all(bar.close, bar.timestamp); //closes any open positions broker.buy(bar.close, 1.0, bar.timestamp, None, None); // can be more complicated with buy, sells, close position, close all etc. } } fn main() { - let data = load_csv("examples/data/eurusd_lse_1h.csv"); - let mut engine = Engine::new(data, 10_000.0, 0.0, 0.00010); + let data = load_csv("data/EURUSD_1H.csv"); + let mut engine = Engine::new(data, 10_000.0, 0.0, 0.00010, 1.0, 1.0); let mut strategy = BuyEveryBar; let stats = engine.run(&mut strategy); println!("{}", stats); } - - - diff --git a/src/data.rs b/src/data.rs index 62a4ab9..cf69d49 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,31 +1,28 @@ -use std::fs::File; -use std::io::{BufRead, BufReader}; use crate::types::Bar; use chrono::DateTime; +use csv::Reader; +use std::fs::File; pub fn load_csv(path: &str) -> Vec { let file = File::open(path).expect("Could not open file"); - let reader = BufReader::new(file); + let mut rdr = Reader::from_reader(file); + let headers = rdr.headers().expect("could not read headers").clone(); + let col = |name: &str| headers.iter().position(|h| h == name).expect(name); let mut bars = Vec::new(); - for line in reader.lines().skip(1) { //skips the header row - let line = line.expect("could not read line"); - let cols: Vec<&str> = line.split(',').collect(); + for result in rdr.records() { + let r = result.expect("could not read record"); + let dt = DateTime::parse_from_rfc3339(&r[0]).expect("Bad timestamp"); - 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 - - let bar = Bar { + bars.push(Bar { timestamp: dt.timestamp(), - open: cols[1].parse().expect("Bad open"), - high: cols[2].parse().expect("Bad high"), - low: cols[3].parse().expect("Bad low"), - close: cols[4].parse().expect("Bad close"), - volume: cols[5].parse().expect("Bad volume"), - }; - - bars.push(bar); // pushes each bar from the csv to an element in Bar - + open: r[col("open")].parse().expect("Bad open"), + high: r[col("high")].parse().expect("Bad high"), + low: r[col("low")].parse().expect("Bad low"), + close: r[col("close")].parse().expect("Bad close"), + volume: r[col("volume")].parse().expect("Bad volume"), + }); } bars -} \ No newline at end of file +}