From f6c60d7b8bacf1a6a80fd510272a2f31d2674cce Mon Sep 17 00:00:00 2001 From: porcelaincode Date: Wed, 28 Jan 2026 06:30:03 +0530 Subject: [PATCH] Initial backtesting engine --- .gitignore | 24 + Cargo.lock | 828 +++++++++++++++ Cargo.toml | 32 + README.md | 794 ++++++++++++++ benches/backtest_benchmark.rs | 123 +++ pyproject.toml | 22 + python/raptorbt/__init__.py | 68 ++ .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 1392 bytes .../raptorbt/_raptorbt.cpython-311-darwin.so | Bin 0 -> 789984 bytes rustfmt.toml | 5 + src/core/error.rs | 91 ++ src/core/mod.rs | 9 + src/core/timeseries.rs | 346 ++++++ src/core/types.rs | 482 +++++++++ src/execution/fees.rs | 160 +++ src/execution/fill.rs | 380 +++++++ src/execution/mod.rs | 9 + src/execution/slippage.rs | 214 ++++ src/indicators/mod.rs | 16 + src/indicators/momentum.rs | 313 ++++++ src/indicators/strength.rs | 265 +++++ src/indicators/trend.rs | 288 +++++ src/indicators/volatility.rs | 259 +++++ src/indicators/volume.rs | 332 ++++++ src/lib.rs | 58 + src/metrics/drawdown.rs | 348 ++++++ src/metrics/mod.rs | 9 + src/metrics/streaming.rs | 404 +++++++ src/metrics/trade_stats.rs | 361 +++++++ src/portfolio/allocation.rs | 345 ++++++ src/portfolio/engine.rs | 869 +++++++++++++++ src/portfolio/mod.rs | 9 + src/portfolio/position.rs | 366 +++++++ src/python/bindings.rs | 988 ++++++++++++++++++ src/python/mod.rs | 4 + src/python/numpy_bridge.rs | 34 + src/signals/expression.rs | 460 ++++++++ src/signals/mod.rs | 10 + src/signals/processor.rs | 449 ++++++++ src/signals/synchronizer.rs | 399 +++++++ src/stops/atr.rs | 242 +++++ src/stops/fixed.rs | 172 +++ src/stops/mod.rs | 38 + src/stops/trailing.rs | 403 +++++++ src/strategies/basket.rs | 480 +++++++++ src/strategies/mod.rs | 13 + src/strategies/multi.rs | 412 ++++++++ src/strategies/options.rs | 450 ++++++++ src/strategies/pairs.rs | 481 +++++++++ src/strategies/single.rs | 206 ++++ tests/test_indicators.rs | 240 +++++ tests/test_portfolio.rs | 314 ++++++ uv.lock | 8 + 53 files changed, 13632 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 benches/backtest_benchmark.rs create mode 100644 pyproject.toml create mode 100644 python/raptorbt/__init__.py create mode 100644 python/raptorbt/__pycache__/__init__.cpython-311.pyc create mode 100755 python/raptorbt/_raptorbt.cpython-311-darwin.so create mode 100644 rustfmt.toml create mode 100644 src/core/error.rs create mode 100644 src/core/mod.rs create mode 100644 src/core/timeseries.rs create mode 100644 src/core/types.rs create mode 100644 src/execution/fees.rs create mode 100644 src/execution/fill.rs create mode 100644 src/execution/mod.rs create mode 100644 src/execution/slippage.rs create mode 100644 src/indicators/mod.rs create mode 100644 src/indicators/momentum.rs create mode 100644 src/indicators/strength.rs create mode 100644 src/indicators/trend.rs create mode 100644 src/indicators/volatility.rs create mode 100644 src/indicators/volume.rs create mode 100644 src/lib.rs create mode 100644 src/metrics/drawdown.rs create mode 100644 src/metrics/mod.rs create mode 100644 src/metrics/streaming.rs create mode 100644 src/metrics/trade_stats.rs create mode 100644 src/portfolio/allocation.rs create mode 100644 src/portfolio/engine.rs create mode 100644 src/portfolio/mod.rs create mode 100644 src/portfolio/position.rs create mode 100644 src/python/bindings.rs create mode 100644 src/python/mod.rs create mode 100644 src/python/numpy_bridge.rs create mode 100644 src/signals/expression.rs create mode 100644 src/signals/mod.rs create mode 100644 src/signals/processor.rs create mode 100644 src/signals/synchronizer.rs create mode 100644 src/stops/atr.rs create mode 100644 src/stops/fixed.rs create mode 100644 src/stops/mod.rs create mode 100644 src/stops/trailing.rs create mode 100644 src/strategies/basket.rs create mode 100644 src/strategies/mod.rs create mode 100644 src/strategies/multi.rs create mode 100644 src/strategies/options.rs create mode 100644 src/strategies/pairs.rs create mode 100644 src/strategies/single.rs create mode 100644 tests/test_indicators.rs create mode 100644 tests/test_portfolio.rs create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e23c5f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Generated by Cargo +# will have compiled files and executables +debug +target + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Python +.venv \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ff45d14 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,828 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "numpy" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef41cbb417ea83b30525259e30ccef6af39b31c240bda578889494c5392d331" +dependencies = [ + "libc", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pyo3", + "rustc-hash", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "raptorbt" +version = "0.1.0" +dependencies = [ + "approx", + "criterion", + "numpy", + "pyo3", + "rayon", + "serde", + "thiserror", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5e99e17 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "raptorbt" +version = "0.1.0" +edition = "2021" +description = "High-performance Rust backtesting engine for quant5" +authors = ["quant5 team"] +license = "MIT" + +[lib] +name = "raptorbt" +crate-type = ["cdylib", "rlib"] + +[dependencies] +pyo3 = { version = "0.20", features = ["extension-module"] } +numpy = "0.20" +rayon = "1.8" +thiserror = "1.0" +serde = { version = "1.0", features = ["derive"] } + +[dev-dependencies] +criterion = "0.5" +approx = "0.5" + +[[bench]] +name = "backtest_benchmark" +harness = false + +[profile.release] +lto = true +codegen-units = 1 +opt-level = 3 +strip = true diff --git a/README.md b/README.md new file mode 100644 index 0000000..66d4daf --- /dev/null +++ b/README.md @@ -0,0 +1,794 @@ +# RaptorBT + +**RaptorBT** is a high-performance backtesting engine written in Rust with Python bindings via PyO3. It serves as a drop-in replacement for VectorBT, providing significant performance improvements while maintaining full metric parity. + +## Table of Contents + +- [Overview](#overview) +- [Performance](#performance) +- [Architecture](#architecture) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Strategy Types](#strategy-types) +- [Metrics](#metrics) +- [Indicators](#indicators) +- [Stop-Loss & Take-Profit](#stop-loss--take-profit) +- [Python Integration](#python-integration) +- [VectorBT Drop-in Replacement](#vectorbt-drop-in-replacement) +- [API Reference](#api-reference) +- [Building from Source](#building-from-source) +- [Testing](#testing) + +--- + +## Overview + +RaptorBT was built to address the performance limitations of VectorBT in production environments: + +| Metric | VectorBT | RaptorBT | Improvement | +| ----------------------------- | ------------------- | ------------ | ------------------------- | +| **Disk Footprint** | ~450MB | <10MB | **45x smaller** | +| **Startup Latency** | 200-600ms | <10ms | **20-60x faster** | +| **Backtest Speed (1K bars)** | 1460ms | 0.25ms | **5,800x faster** | +| **Backtest Speed (50K bars)** | 43ms | 1.7ms | **25x faster** | +| **Memory Usage** | High (JIT + pandas) | Low (native) | **Significant reduction** | + +### Key Features + +- **5 Strategy Types**: Single instrument, basket/collective, pairs trading, options, and multi-strategy +- **30+ Metrics**: Full parity with VectorBT including Sharpe, Sortino, Calmar, Omega, SQN, and more +- **10 Technical Indicators**: SMA, EMA, RSI, MACD, Stochastic, ATR, Bollinger Bands, ADX, VWAP, Supertrend +- **Stop/Target Management**: Fixed, ATR-based, and trailing stops with risk-reward targets +- **100% Deterministic**: No JIT compilation variance between runs +- **Native Parallelism**: Rayon-based parallel processing with explicit SIMD optimizations + +--- + +## Performance + +### Benchmark Results + +Tested on Apple Silicon M-series with random walk price data and SMA crossover strategy: + +``` +┌─────────────┬────────────┬───────────┬──────────┐ +│ Data Size │ VectorBT │ RaptorBT │ Speedup │ +├─────────────┼────────────┼───────────┼──────────┤ +│ 1,000 bars │ 1,460 ms │ 0.25 ms │ 5,827x │ +│ 5,000 bars │ 36 ms │ 0.24 ms │ 153x │ +│ 10,000 bars │ 37 ms │ 0.46 ms │ 80x │ +│ 50,000 bars │ 43 ms │ 1.68 ms │ 26x │ +└─────────────┴────────────┴───────────┴──────────┘ +``` + +> **Note**: First VectorBT run includes Numba JIT compilation overhead. Subsequent runs are faster but still significantly slower than RaptorBT. + +### Metric Accuracy + +RaptorBT produces **identical results** to VectorBT: + +``` +VectorBT Total Return: 7.2764% +RaptorBT Total Return: 7.2764% +Difference: 0.0000% ✓ +``` + +--- + +## Architecture + +``` +raptorbt/ +├── src/ +│ ├── core/ # Core types and error handling +│ │ ├── types.rs # BacktestConfig, BacktestResult, Trade, Metrics +│ │ ├── error.rs # RaptorError enum +│ │ └── timeseries.rs # Time series utilities +│ │ +│ ├── strategies/ # Strategy implementations +│ │ ├── single.rs # Single instrument backtest +│ │ ├── basket.rs # Basket/collective strategies +│ │ ├── pairs.rs # Pairs trading +│ │ ├── options.rs # Options strategies +│ │ └── multi.rs # Multi-strategy combining +│ │ +│ ├── indicators/ # Technical indicators +│ │ ├── trend.rs # SMA, EMA, Supertrend +│ │ ├── momentum.rs # RSI, MACD, Stochastic +│ │ ├── volatility.rs # ATR, Bollinger Bands +│ │ ├── strength.rs # ADX +│ │ └── volume.rs # VWAP +│ │ +│ ├── metrics/ # Performance metrics +│ │ ├── streaming.rs # Streaming metric calculations +│ │ ├── drawdown.rs # Drawdown analysis +│ │ └── trade_stats.rs # Trade statistics +│ │ +│ ├── signals/ # Signal processing +│ │ ├── processor.rs # Entry/exit signal processing +│ │ ├── synchronizer.rs # Multi-instrument sync +│ │ └── expression.rs # Signal expressions +│ │ +│ ├── stops/ # Stop-loss implementations +│ │ ├── fixed.rs # Fixed percentage stops +│ │ ├── atr.rs # ATR-based stops +│ │ └── trailing.rs # Trailing stops +│ │ +│ ├── python/ # PyO3 bindings +│ │ ├── bindings.rs # Python function exports +│ │ └── numpy_bridge.rs # NumPy array conversion +│ │ +│ └── lib.rs # Library entry point +│ +├── Cargo.toml # Rust dependencies +└── pyproject.toml # Python package config +``` + +--- + +## Installation + +### From Pre-built Wheel + +```bash +pip install raptorbt +``` + +### From Source + +```bash +cd raptorbt +maturin develop --release +``` + +### Verify Installation + +```python +import raptorbt +print("RaptorBT installed successfully!") +``` + +--- + +## Quick Start + +### Basic Single Instrument Backtest + +```python +import numpy as np +import pandas as pd +import raptorbt + +# Prepare data +df = pd.read_csv("your_data.csv", index_col=0, parse_dates=True) + +# Generate signals (SMA crossover example) +sma_fast = df['close'].rolling(10).mean() +sma_slow = df['close'].rolling(20).mean() +entries = (sma_fast > sma_slow) & (sma_fast.shift(1) <= sma_slow.shift(1)) +exits = (sma_fast < sma_slow) & (sma_fast.shift(1) >= sma_slow.shift(1)) + +# Configure backtest +config = raptorbt.PyBacktestConfig( + initial_capital=100000, + fees=0.001, # 0.1% per trade + slippage=0.0005, # 0.05% slippage + upon_bar_close=True +) + +# Optional: Add stop-loss +config.set_fixed_stop(0.02) # 2% stop-loss + +# Optional: Add take-profit +config.set_fixed_target(0.04) # 4% take-profit + +# Run backtest +result = raptorbt.run_single_backtest( + timestamps=df.index.astype('int64').values, + open=df['open'].values, + high=df['high'].values, + low=df['low'].values, + close=df['close'].values, + volume=df['volume'].values, + entries=entries.values, + exits=exits.values, + direction=1, # 1 = Long, -1 = Short + weight=1.0, + symbol="AAPL", + config=config, +) + +# Access results +print(f"Total Return: {result.metrics.total_return_pct:.2f}%") +print(f"Sharpe Ratio: {result.metrics.sharpe_ratio:.2f}") +print(f"Max Drawdown: {result.metrics.max_drawdown_pct:.2f}%") +print(f"Win Rate: {result.metrics.win_rate_pct:.2f}%") +print(f"Total Trades: {result.metrics.total_trades}") + +# Get equity curve +equity = result.equity_curve() # Returns numpy array + +# Get trades +trades = result.trades() # Returns list of PyTrade objects +``` + +--- + +## Strategy Types + +### 1. Single Instrument + +Basic long or short strategy on a single instrument. + +```python +result = raptorbt.run_single_backtest( + timestamps=timestamps, + open=open_prices, high=high_prices, low=low_prices, + close=close_prices, volume=volume, + entries=entries, exits=exits, + direction=1, # 1=Long, -1=Short + weight=1.0, + symbol="SYMBOL", + config=config, +) +``` + +### 2. Basket/Collective + +Trade multiple instruments with synchronized signals. + +```python +instruments = [ + (timestamps, open1, high1, low1, close1, volume1, entries1, exits1, 1, 0.33, "AAPL"), + (timestamps, open2, high2, low2, close2, volume2, entries2, exits2, 1, 0.33, "GOOGL"), + (timestamps, open3, high3, low3, close3, volume3, entries3, exits3, 1, 0.34, "MSFT"), +] + +result = raptorbt.run_basket_backtest( + instruments=instruments, + config=config, + sync_mode="all", # "all", "any", "majority", "master" +) +``` + +**Sync Modes:** + +- `all`: Enter only when ALL instruments signal +- `any`: Enter when ANY instrument signals +- `majority`: Enter when >50% of instruments signal +- `master`: Follow the first instrument's signals + +### 3. Pairs Trading + +Long one instrument, short another with optional hedge ratio. + +```python +result = raptorbt.run_pairs_backtest( + # Long leg + leg1_timestamps=timestamps, + leg1_open=long_open, leg1_high=long_high, + leg1_low=long_low, leg1_close=long_close, + leg1_volume=long_volume, + # Short leg + leg2_timestamps=timestamps, + leg2_open=short_open, leg2_high=short_high, + leg2_low=short_low, leg2_close=short_close, + leg2_volume=short_volume, + # Signals + entries=entries, exits=exits, + direction=1, + symbol="TCS_INFY", + config=config, + hedge_ratio=1.5, # Short 1.5x the long position + dynamic_hedge=False, # Use rolling hedge ratio +) +``` + +### 4. Options + +Backtest options strategies with strike selection. + +```python +result = raptorbt.run_options_backtest( + timestamps=timestamps, + open=underlying_open, high=underlying_high, + low=underlying_low, close=underlying_close, + volume=volume, + option_prices=option_prices, # Option premium series + entries=entries, exits=exits, + direction=1, + symbol="NIFTY_CE", + config=config, + option_type="call", # "call" or "put" + strike_selection="atm", # "atm", "otm1", "otm2", "itm1", "itm2" + size_type="percent", # "percent", "contracts", "notional", "risk" + size_value=0.1, # 10% of capital + lot_size=50, # Options lot size + strike_interval=50.0, # Strike interval (e.g., 50 for NIFTY) +) +``` + +### 5. Multi-Strategy + +Combine multiple strategies on the same instrument. + +```python +strategies = [ + (entries_sma, exits_sma, 1, 0.4, "SMA_Crossover"), # 40% weight + (entries_rsi, exits_rsi, 1, 0.35, "RSI_MeanRev"), # 35% weight + (entries_bb, exits_bb, 1, 0.25, "BB_Breakout"), # 25% weight +] + +result = raptorbt.run_multi_backtest( + timestamps=timestamps, + open=open_prices, high=high_prices, + low=low_prices, close=close_prices, + volume=volume, + strategies=strategies, + config=config, + combine_mode="any", # "any", "all", "majority", "weighted", "independent" +) +``` + +**Combine Modes:** + +- `any`: Enter when any strategy signals +- `all`: Enter only when all strategies signal +- `majority`: Enter when >50% of strategies signal +- `weighted`: Weight signals by strategy weight +- `independent`: Run strategies independently (aggregate PnL) + +--- + +## Metrics + +RaptorBT calculates 30+ performance metrics: + +### Core Performance + +| Metric | Description | +| ------------------ | --------------------------------- | +| `total_return_pct` | Total return as percentage | +| `sharpe_ratio` | Risk-adjusted return (annualized) | +| `sortino_ratio` | Downside risk-adjusted return | +| `calmar_ratio` | Return / Max Drawdown | +| `omega_ratio` | Probability-weighted gains/losses | + +### Drawdown + +| Metric | Description | +| ----------------------- | ------------------------------ | +| `max_drawdown_pct` | Maximum peak-to-trough decline | +| `max_drawdown_duration` | Longest drawdown period (bars) | + +### Trade Statistics + +| Metric | Description | +| --------------------- | ---------------------------- | +| `total_trades` | Total number of trades | +| `total_closed_trades` | Number of closed trades | +| `total_open_trades` | Number of open positions | +| `winning_trades` | Number of profitable trades | +| `losing_trades` | Number of losing trades | +| `win_rate_pct` | Percentage of winning trades | + +### Trade Performance + +| Metric | Description | +| ---------------------- | --------------------------------- | +| `profit_factor` | Gross profit / Gross loss | +| `expectancy` | Average expected profit per trade | +| `sqn` | System Quality Number | +| `avg_trade_return_pct` | Average trade return | +| `avg_win_pct` | Average winning trade return | +| `avg_loss_pct` | Average losing trade return | +| `best_trade_pct` | Best single trade return | +| `worst_trade_pct` | Worst single trade return | + +### Duration + +| Metric | Description | +| ---------------------- | ------------------------------ | +| `avg_holding_period` | Average trade duration (bars) | +| `avg_winning_duration` | Average winning trade duration | +| `avg_losing_duration` | Average losing trade duration | + +### Streaks + +| Metric | Description | +| ------------------------ | ---------------------- | +| `max_consecutive_wins` | Longest winning streak | +| `max_consecutive_losses` | Longest losing streak | + +### Other + +| Metric | Description | +| ----------------- | ---------------------------------- | +| `start_value` | Initial portfolio value | +| `end_value` | Final portfolio value | +| `total_fees_paid` | Total transaction costs | +| `open_trade_pnl` | Unrealized PnL from open positions | +| `exposure_pct` | Percentage of time in market | + +--- + +## Indicators + +RaptorBT includes optimized technical indicators: + +```python +import raptorbt + +# Trend indicators +sma = raptorbt.sma(close, period=20) +ema = raptorbt.ema(close, period=20) +supertrend, direction = raptorbt.supertrend(high, low, close, period=10, multiplier=3.0) + +# Momentum indicators +rsi = raptorbt.rsi(close, period=14) +macd_line, signal_line, histogram = raptorbt.macd(close, fast=12, slow=26, signal=9) +stoch_k, stoch_d = raptorbt.stochastic(high, low, close, k_period=14, d_period=3) + +# Volatility indicators +atr = raptorbt.atr(high, low, close, period=14) +upper, middle, lower = raptorbt.bollinger_bands(close, period=20, std_dev=2.0) + +# Strength indicators +adx = raptorbt.adx(high, low, close, period=14) + +# Volume indicators +vwap = raptorbt.vwap(high, low, close, volume) +``` + +--- + +## Stop-Loss & Take-Profit + +### Fixed Percentage + +```python +config = raptorbt.PyBacktestConfig(initial_capital=100000, fees=0.001) +config.set_fixed_stop(0.02) # 2% stop-loss +config.set_fixed_target(0.04) # 4% take-profit +``` + +### ATR-Based + +```python +config.set_atr_stop(multiplier=2.0, period=14) # 2x ATR stop +config.set_atr_target(multiplier=3.0, period=14) # 3x ATR target +``` + +### Trailing Stop + +```python +config.set_trailing_stop(0.02) # 2% trailing stop +``` + +### Risk-Reward Target + +```python +config.set_risk_reward_target(ratio=2.0) # 2:1 risk-reward ratio +``` + +--- + +## Python Integration + +RaptorBT integrates seamlessly with the quant5 golf runner through `rpbt.py`. + +### Enable RaptorBT + +```bash +export USE_RAPTORBT=1 +``` + +Or in Python: + +```python +import os +os.environ["USE_RAPTORBT"] = "1" +``` + +### Integration Functions + +```python +from app.engine.golf.rpbt import ( + is_raptorbt_enabled, + RaptorBTConfig, + RaptorBTPortfolioWrapper, + run_single_backtest_raptorbt, + run_basket_backtest_raptorbt, + run_pairs_backtest_raptorbt, + run_options_backtest_raptorbt, + run_multi_backtest_raptorbt, +) + +# Check if RaptorBT is enabled +if is_raptorbt_enabled(): + print("Using RaptorBT backend") +``` + +--- + +## VectorBT Drop-in Replacement + +RaptorBT provides a `RaptorBTPortfolioWrapper` that mimics the VectorBT Portfolio interface: + +```python +from app.engine.golf.rpbt import ( + RaptorBTPortfolioWrapper, + run_single_backtest_raptorbt, + RaptorBTConfig, +) + +# Run backtest +result = run_single_backtest_raptorbt(compiled, ohlcv_df, config, symbol) + +# Wrap result for VectorBT compatibility +portfolio = RaptorBTPortfolioWrapper(result) + +# Use like VectorBT Portfolio +stats = portfolio.stats() # Returns pd.Series with VectorBT-format keys +equity = portfolio.value() # Returns equity curve as pd.Series +dd = portfolio.drawdown() # Returns drawdown curve as pd.Series +trades_df = portfolio.trades() # Returns trades as pd.DataFrame + +# Access properties +print(portfolio.total_return) # Total return percentage +print(portfolio.sharpe_ratio) # Sharpe ratio +print(portfolio.max_drawdown) # Max drawdown percentage +print(portfolio.win_rate) # Win rate percentage +print(portfolio.profit_factor) # Profit factor +print(portfolio.sqn) # System Quality Number +print(portfolio.expectancy) # Expected value per trade +print(portfolio.omega_ratio) # Omega ratio +``` + +### Stats Format + +The `stats()` method returns a pandas Series with VectorBT-compatible keys: + +```python +stats = portfolio.stats() +print(stats["Total Return [%]"]) +print(stats["Sharpe Ratio"]) +print(stats["Max Drawdown [%]"]) +print(stats["Win Rate [%]"]) +print(stats["Profit Factor"]) +print(stats["SQN"]) +print(stats["Omega Ratio"]) +# ... and 20+ more metrics +``` + +--- + +## API Reference + +### PyBacktestConfig + +```python +config = raptorbt.PyBacktestConfig( + initial_capital: float = 100000.0, + fees: float = 0.001, + slippage: float = 0.0, + upon_bar_close: bool = True, +) + +# Stop methods +config.set_fixed_stop(percent: float) +config.set_atr_stop(multiplier: float, period: int) +config.set_trailing_stop(percent: float) + +# Target methods +config.set_fixed_target(percent: float) +config.set_atr_target(multiplier: float, period: int) +config.set_risk_reward_target(ratio: float) +``` + +### PyBacktestResult + +```python +result = raptorbt.run_single_backtest(...) + +# Attributes +result.metrics # PyBacktestMetrics object + +# Methods +result.equity_curve() # numpy.ndarray +result.drawdown_curve() # numpy.ndarray +result.returns() # numpy.ndarray +result.trades() # List[PyTrade] +``` + +### PyBacktestMetrics + +```python +metrics = result.metrics + +# All available metrics +metrics.total_return_pct +metrics.sharpe_ratio +metrics.sortino_ratio +metrics.calmar_ratio +metrics.omega_ratio +metrics.max_drawdown_pct +metrics.max_drawdown_duration +metrics.win_rate_pct +metrics.profit_factor +metrics.expectancy +metrics.sqn +metrics.total_trades +metrics.total_closed_trades +metrics.total_open_trades +metrics.winning_trades +metrics.losing_trades +metrics.start_value +metrics.end_value +metrics.total_fees_paid +metrics.best_trade_pct +metrics.worst_trade_pct +metrics.avg_trade_return_pct +metrics.avg_win_pct +metrics.avg_loss_pct +metrics.avg_holding_period +metrics.avg_winning_duration +metrics.avg_losing_duration +metrics.max_consecutive_wins +metrics.max_consecutive_losses +metrics.exposure_pct +metrics.open_trade_pnl + +# Convert to dictionary (VectorBT format) +stats_dict = metrics.to_dict() +``` + +### PyTrade + +```python +for trade in result.trades(): + print(trade.id) # Trade ID + print(trade.symbol) # Symbol + print(trade.entry_idx) # Entry bar index + print(trade.exit_idx) # Exit bar index + print(trade.entry_price) # Entry price + print(trade.exit_price) # Exit price + print(trade.size) # Position size + print(trade.direction) # 1=Long, -1=Short + print(trade.pnl) # Profit/Loss + print(trade.return_pct) # Return percentage + print(trade.fees) # Fees paid + print(trade.exit_reason) # "Signal", "StopLoss", "TakeProfit" +``` + +--- + +## Building from Source + +### Prerequisites + +- Rust 1.70+ (install via [rustup](https://rustup.rs/)) +- Python 3.10+ +- maturin (`pip install maturin`) + +### Development Build + +```bash +cd raptorbt +maturin develop --release +``` + +### Production Build + +```bash +cd raptorbt +maturin build --release +pip install target/wheels/raptorbt-*.whl +``` + +### Using the Build Script + +```bash +./scripts/build-engine.sh --install +``` + +--- + +## Testing + +### Rust Unit Tests + +```bash +cd raptorbt +cargo test +``` + +### Python Integration Tests + +```bash +# Test basic functionality +uv run python -c " +import raptorbt +import numpy as np + +config = raptorbt.PyBacktestConfig(initial_capital=100000, fees=0.001) +result = raptorbt.run_single_backtest( + timestamps=np.arange(100, dtype=np.int64), + open=np.random.randn(100).cumsum() + 100, + high=np.random.randn(100).cumsum() + 101, + low=np.random.randn(100).cumsum() + 99, + close=np.random.randn(100).cumsum() + 100, + volume=np.ones(100), + entries=np.array([i % 20 == 0 for i in range(100)]), + exits=np.array([i % 20 == 10 for i in range(100)]), + direction=1, + weight=1.0, + symbol='TEST', + config=config, +) +print(f'Total Return: {result.metrics.total_return_pct:.2f}%') +print('RaptorBT is working correctly!') +" +``` + +### Comparison Test (VectorBT vs RaptorBT) + +```bash +USE_RAPTORBT=1 uv run python << 'EOF' +import numpy as np +import pandas as pd +import vectorbt as vbt +import raptorbt + +# Create test data +np.random.seed(42) +n = 500 +dates = pd.date_range('2023-01-01', periods=n, freq='D') +close = np.cumprod(1 + np.random.randn(n) * 0.02) * 100 +entries = np.zeros(n, dtype=bool) +exits = np.zeros(n, dtype=bool) +entries[::20] = True +exits[10::20] = True + +# VectorBT +pf = vbt.Portfolio.from_signals( + close=pd.Series(close, index=dates), + entries=pd.Series(entries, index=dates), + exits=pd.Series(exits, index=dates), + init_cash=100000, fees=0.001 +) + +# RaptorBT +config = raptorbt.PyBacktestConfig(initial_capital=100000, fees=0.001) +result = raptorbt.run_single_backtest( + timestamps=dates.astype('int64').values, + open=close, high=close, low=close, close=close, + volume=np.ones(n), entries=entries, exits=exits, + direction=1, weight=1.0, symbol="TEST", config=config +) + +print(f"VectorBT: {pf.stats()['Total Return [%]']:.4f}%") +print(f"RaptorBT: {result.metrics.total_return_pct:.4f}%") +print(f"Match: {abs(pf.stats()['Total Return [%]'] - result.metrics.total_return_pct) < 0.01}") +EOF +``` + +--- + +## License + +RaptorBT is proprietary software developed for the quant5 platform. + +--- + +## Changelog + +### v0.1.0 (2024-01) + +- Initial release +- 5 strategy types: single, basket, pairs, options, multi +- 30+ performance metrics +- 10 technical indicators +- Fixed, ATR, and trailing stops +- PyO3 Python bindings +- VectorBT-compatible wrapper diff --git a/benches/backtest_benchmark.rs b/benches/backtest_benchmark.rs new file mode 100644 index 0000000..7280c1d --- /dev/null +++ b/benches/backtest_benchmark.rs @@ -0,0 +1,123 @@ +//! Benchmark for RaptorBT backtesting performance. + +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use raptorbt::core::types::{BacktestConfig, CompiledSignals, Direction, OhlcvData}; +use raptorbt::indicators::trend::{ema, sma}; +use raptorbt::portfolio::engine::PortfolioEngine; + +/// Generate sample OHLCV data. +fn generate_sample_data(n: usize) -> OhlcvData { + let mut open = vec![100.0; n]; + let mut high = vec![101.0; n]; + let mut low = vec![99.0; n]; + let mut close = vec![100.0; n]; + + // Create a trending pattern + for i in 1..n { + let change = (i as f64 * 0.1).sin() * 2.0; + close[i] = close[i - 1] + change; + open[i] = close[i - 1]; + high[i] = close[i].max(open[i]) + 1.0; + low[i] = close[i].min(open[i]) - 1.0; + } + + OhlcvData { + timestamps: (0..n as i64).collect(), + open, + high, + low, + close, + volume: vec![1000.0; n], + } +} + +/// Generate sample trading signals based on SMA crossover. +fn generate_sample_signals( + close: &[f64], + fast_period: usize, + slow_period: usize, +) -> CompiledSignals { + let n = close.len(); + let fast_sma = sma(close, fast_period).unwrap_or_else(|_| vec![0.0; n]); + let slow_sma = sma(close, slow_period).unwrap_or_else(|_| vec![0.0; n]); + + let mut entries = vec![false; n]; + let mut exits = vec![false; n]; + + for i in 1..n { + // Entry: fast crosses above slow + if fast_sma[i] > slow_sma[i] && fast_sma[i - 1] <= slow_sma[i - 1] { + entries[i] = true; + } + // Exit: fast crosses below slow + if fast_sma[i] < slow_sma[i] && fast_sma[i - 1] >= slow_sma[i - 1] { + exits[i] = true; + } + } + + CompiledSignals { + symbol: "BENCH".to_string(), + entries, + exits, + position_sizes: None, + direction: Direction::Long, + weight: 1.0, + } +} + +fn bench_single_backtest(c: &mut Criterion) { + let mut group = c.benchmark_group("single_backtest"); + + for size in [1000, 5000, 10000, 50000].iter() { + group.bench_with_input(BenchmarkId::new("bars", size), size, |b, &size| { + let ohlcv = generate_sample_data(size); + let signals = generate_sample_signals(&ohlcv.close, 10, 30); + let config = BacktestConfig::default(); + let engine = PortfolioEngine::new(config); + + b.iter(|| { + let result = engine.run_single(black_box(&ohlcv), black_box(&signals)); + black_box(result) + }); + }); + } + + group.finish(); +} + +fn bench_sma(c: &mut Criterion) { + let mut group = c.benchmark_group("sma"); + + for size in [1000, 5000, 10000, 50000].iter() { + group.bench_with_input(BenchmarkId::new("data_size", size), size, |b, &size| { + let ohlcv = generate_sample_data(size); + + b.iter(|| { + let result = sma(black_box(&ohlcv.close), black_box(20)); + black_box(result) + }); + }); + } + + group.finish(); +} + +fn bench_ema(c: &mut Criterion) { + let mut group = c.benchmark_group("ema"); + + for size in [1000, 5000, 10000, 50000].iter() { + group.bench_with_input(BenchmarkId::new("data_size", size), size, |b, &size| { + let ohlcv = generate_sample_data(size); + + b.iter(|| { + let result = ema(black_box(&ohlcv.close), black_box(20)); + black_box(result) + }); + }); + } + + group.finish(); +} + +criterion_group!(benches, bench_single_backtest, bench_sma, bench_ema); +criterion_main!(benches); diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..adfbca4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["maturin>=1.4,<2.0"] +build-backend = "maturin" + +[project] +name = "raptorbt" +version = "0.1.0" +description = "High-performance Rust backtesting engine for quant5" +readme = "README.md" +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +[tool.maturin] +features = ["pyo3/extension-module"] +python-source = "python" +module-name = "raptorbt._raptorbt" diff --git a/python/raptorbt/__init__.py b/python/raptorbt/__init__.py new file mode 100644 index 0000000..f51aabd --- /dev/null +++ b/python/raptorbt/__init__.py @@ -0,0 +1,68 @@ +""" +RaptorBT - High-performance Rust backtesting engine for quant5. + +This module provides Python bindings for the Rust-based backtesting engine, +offering significant performance improvements over vectorbt: +- Disk footprint: <10MB (vs vectorbt's ~450MB) +- Startup latency: <10ms (vs 200-600ms) +- 100% deterministic execution (no JIT cache) +- Native parallelism via Rayon + explicit SIMD +""" + +from raptorbt._raptorbt import ( + # Config classes + PyBacktestConfig, + PyStopConfig, + PyTargetConfig, + # Result classes + PyBacktestResult, + PyBacktestMetrics, + PyTrade, + # Backtest functions + run_single_backtest, + run_basket_backtest, + run_options_backtest, + run_pairs_backtest, + run_multi_backtest, + # Indicator functions + sma, + ema, + rsi, + macd, + stochastic, + atr, + bollinger_bands, + adx, + vwap, + supertrend, +) + +__version__ = "0.1.0" + +__all__ = [ + # Config classes + "PyBacktestConfig", + "PyStopConfig", + "PyTargetConfig", + # Result classes + "PyBacktestResult", + "PyBacktestMetrics", + "PyTrade", + # Backtest functions + "run_single_backtest", + "run_basket_backtest", + "run_options_backtest", + "run_pairs_backtest", + "run_multi_backtest", + # Indicator functions + "sma", + "ema", + "rsi", + "macd", + "stochastic", + "atr", + "bollinger_bands", + "adx", + "vwap", + "supertrend", +] diff --git a/python/raptorbt/__pycache__/__init__.cpython-311.pyc b/python/raptorbt/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b49869a2f03fb831e37e3132b27b3d9602e0b105 GIT binary patch literal 1392 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

Yp_(@= zWWEK@3i;p>&!qly$b)YKe1-E)@vWpEL>zSgd)672lfHp^NPi0HMO#PNqvu)H^8QzH z=5WR|LC@&Eg(&o@XkgQZ#%`g}No{cOKM`Ah+C&@jX+wT__mJk+7V4A?^q;45&onTP ze8T-g;w5*pXn!Mjy_FIcZQ+GS5Dp}T&m*k)?c~q4hZ2@fd?z##`z3EqS@vr#6#X@> zBC$v|a*B^YV@iHA>Bht&>5montP4f2A%9<~x)g8dH}F1{O(<>7N)@t#Byn7h$p|pf`&K;1J zICqU2A1cWdYoSk%C%t?_i-D5{?01L|4QvrzVrB|w5rFn2^`ff$zIpo#2OzMTum86Bm;i&dVEC+yl{^F8^W)f zZ&~(p{GW*rvib)|`b_)R#G&)&KlVTTm#lRW^{ZaVC@Q8T7G3D#5vlfv-lIN+e>&Mo zLpjj*x%Qjni85B}p{K>KtR*~zz(51OZ7wQs}zSslasOMRh8sukXRf-g)TEa-fu@2Guq3Z@_Eo6`}TTW)`qeb&c*o5H^9 zXaDVhyxOTJu`cZZzZ|_8n;7Kz<)qZVI>o;s;`h}T%NHloJ@irm{ww^pR$HM{*$?{u zP&Rtw%{xUw-YIIBQ&|+`og&>er}>9WC;lmY*T{G)U*$q4ePLg{QFL{z5#DNrTjpm^ zI^Wv+Zlnrwz|0V30gWPxB&`EC;jfsV;%VKYx=)6(%R$nVz zk>rol|76D>=l9^VLUnWAt#s>tH>vJ5N&Ywo)a|`d zp|LZ5@!Sg@tgbAotG))gKt}B4>Jw#izd-kqmx8nIBR?N#?jvWeg{sh!dH=f ztWv)fUSet(H-aA^Cvf&x}E%zAv?fQ1@51!V(-?HsI z-F72V=&i%8zt-?#n5SQ)`>oQMg0kFxZ3T{#1n1nR<_t$tDawv`qcEN_tp{pxt9L?13rV_ zv~G1SbiPh!Otu{`5T3-q47&qR=S8{q)=~0&_ue|9o)Y$_=Z;X%AK33#t3A-TuewnTmiPrfYW`2j6-*rbZztfrDDa>!lcx2mN>t9e*Q(esZ=pJi4U2n3S7|Rb> zD+*Z`o_%#KGMCrfc0N<>-%wcTv~@i;*5#Bjsr@?e-esXbHE!dN(4V#b4jZ0@4p84b zZQ7n=|J1;2`zHow+6xWLu%9q6-Ch6`f3Lm?(KlNA+UvCT^Z(Y?H{RM`c@%x~&P3lr z(U`AkY~fp$B9~M7#ow1b^7vOw+2*|=cYKrQ(DF`>O)_ns!yM{Co9p`B6%NujI@i|! z+Ief|EzUNzcbePY8%%q0?CT87wy!lX)4tlk411h`>2?|LY;Z#k{9fP%UuY`*DjARP zyV7mHxA*zB9Nhk++4~-){m>~Hb^Y!)Hph`=zRPI^x1 z>vSKTe4r!Me=O4Z8odyjjk8tfy68fcdF|Vd%`xXZZ%JGgEByC?HpibKYxVZ-Z!@>A z1OJPu<5==uO5UPl%=OGyjvVS<1ym{o6&GE71?M`0tSJHopP8AftCMZ5Z zP_(q5#!b-B|JOQu*Kz}~|1&Vte%`r z#X}0p218JHmJ5nM5>y$lQO0t@A<7VqZsM7rKb`FC9rI1yIX1VKnloUoftmJ024>jR z2BzCpz!W#Xxql(Oy{#m6{k#e3m0#(Wf0xOhW8Yz5wtc&Snf7!8Gwf*wrrT42+t{}~ zydMK^@vu|ewja5lbL^{4S=|3`V5VJWV1|8_f$8=YK-mU)b=*xIQwd+>)^SSGx9NP7 zZBVKEtyj{woB1}FaFP2>GB=fZW>T8#Nu#kGy&WE+tyCHy~V_5*&p&|VdREy;N9}iIcnZ*1w3K$y$U~3xuqR8wA_9i zG+0+?vE!l1PJnmo7T%BD!GEzS+Ja5dX6^;rDZVXXg?eVCpDk6VR=rw<+d+ zX|GP*86WZ26h*f9LK#05ua8^^y1?-H+I3oWG|CR~?Tw0FY!;TUvd1@x8l-xhkw#gvoR*|AZ`zS;`s1Z0Ysd4* z#_6*i@WW*l++nDFu}`M8;~m9!w{}Qg)CYMvy6G~>5&Gm>J8Jnax<`1{`mc@MNBPNk z-$i)#2Ud72b_^@~riXMkr-8>kxAu=!(1!i9u3*kw&fK|-IdmyB{3T)LR7iAuCF53= zG;Wi`FY>0jg$`>A?VLkdA5Ht_af^zkr9TRwMZ?Sq;W~nO;rlXC*3b#N8~+^asV2ml zdSY910{3LQe(UgT%Uzyrxp=nCjG=h88?JX`hmtM&Xh#`gooz-uwqLEiBHjKkai3A&VdXGa zrXKZLUW1$`c`YwfxyQDa&7B(U*YfZ0Kl-)22zv^%?hmn+D;_@LT3*n8Ess0GoM1gH zZ@mNMDA)4Rrw(4rzk#nj(pt{v&ban#`8SN?w^_>-6AoC*%e}Q+M>+qSYxzOSPhQKF zg#S0!aunY*Z$CWL9_+2AZ0AS{_U~ty}C=TG#Ua@y=S_LEJv-JM3Dn$~x+`{4O%u+TGst9-)1e}bM2wm z@=D72-(1Tf%1>U)j}iXgT+6$s9^G0V&N=!2V=do5;lQmQP}BB(LR* z=?8D^YAqjkn6+F?I;~|t&^wnqyKDJt?ruo^rnUUSDM!7QKf=~4c`g5rb|gwWLSvtR zjI$dwQg>*j6QP;Xp|dlfvpXI{XG>>*9nb9%e~R=X3-Tj5Ay0>~6}stR=%#<(%Q{8Q zv>tt#=ws2^wJxpwF*3VGN7wrk1?aEd2u?>2eZ~9*OV4J*Z4R16=m-#{s zl($B7+uRP4tuD{+km}HEr74KdDzCatbHkO}n0+3(O*>k<^?V-P)sZ*9#2%b1ldL)p zy57)lbGdiu8r}?oejEA?^c#4Ie*64d^w!vv{S-VTQ>{vB%cV(eDMqG$$hMquU|Y`n z7Hw(jH~Vi+0WX&Y}6Cam*tlyB0p!_nk+%5$1q+p-bU|?uHJ_fzK#}E|whB`!>@3b~SAn z#;+&8KK|V1waD&eBe-`AwvpIEMzMt~9MWbBxgPue=;gt96no8Akt;9xv23t=*f+2~ z_rP;U`dIPuv#k}0&eo1n^ig9AtPT0n*#%Ds?SkJE{k<6;Z4&8gQZnZYzjMJ&<@BIm z#XnulxALo*^1H=JxO)S1^p|5=O`vJLF*9XpGrYiJ8& zj4i>&OqEODsoZ7AW#wnm0^c((sgEVk*epE~eNdN@HGhvulVPi$z52Ni?ST4Up#ELd zzZd>2kNS7-^o5ggSPu>(z#+l+41+_4!NCs>*B(%}#U7AbOLq1{$X3(X~Y>C0NrfembUnQ#7pjzY-6;-)Ww{DU$24J zt%G;h|2qD!cmJ>F{|d@~nL5iE$Fs>(nP;sKO;KC@A!ZU)=3ce54CZbpD_+Sss;%mC zwO9SFHu(1?%G5W)TRLmmh;Kn|{t&#?tIO^Szw+Vz;fV#-j!A0kGRySgfobtaR@#q9 zyVFg}dh4HRZIDf??2*?ZujtEImGH*4?1T$BTLMcnL)+-LK2~P);7aI%E0CL9 zj@+c9k(+2-KY?yYJ}-Tn5{Elu%$wQBI~KU|js-^E;hvX$ukkIs1$@1J*Z62GzH`dF zr!_Cu(uSoornZeQcHy$$Rhl_`Yr=kQ3IC@u&ffTXV>{c|vsvfSB>$BEv2rH$uAMoc zrG1({8h>PXDfqwS@y>^@8}BD!|5$tuchviP&JQ1-Hot$qwLx>RWJX0x5Z@7ewaYB# zQy(k4c{1}oh4(%Ru=6e89SgmM(HGlftwZuL2Lomfwy(DqJ6+A=H)+Ef{wL4h2>HJt z@4Ni!`8_IoTiWN1<;(Z$ykY(^CrAA&u|j9i3&eebuE~=dy?M2xSIA?{%VzB-guBwu zyV(;rGp7UmS6bOwN{6dC!`fR=63GbdW}V0etqWt5=gFQJuODBZSTvb*Hxh4XRO03l zcRg_vlj0sEPWM?{>&8h}tvywF%$mXONw$3^%$g}o4%4n0{PKpn-!<2M3vGQq;gOVg zh8yqMqCH^dccy&@?N`|S*RNEVwClen%-(RR!sMwFe$1WGguVK;4g)E~vkps% z7v0|xX!?}0jw_STkkdaZJ3v^nckg=#!pall4Rs%Va#$X}`_c3Ge?^y&%onV>nsWV} z;0sP<{im}JWUwD(vM*%87i8n>dmvx1i#a9xfn?jMd}CYLv(0A8?+g8!`S{nn9GXAi zv$#`_d#>OObSGVb;A#cUQ`p_)erBUjpr^4} zslZlheY#8klSjOl?8{8waK2SyBNp+`HTFYMc$u9JFY}Fi^K0XM3$K9w_h{^@G{=UZ zXH^nUhCRCaI=K(>B}cDy??I;hDeN<8$JqW$+qJ)Tc>DI}y7of{_S=Yd`JkW=+i!u+ zPQRViuHTBWjXIWon@+!_&~KY)w`?zb^qXgA_fBO}-?=` z&2#o}bLVUJhU7EaI;VXf9J=qex67AzP`;!&H_#eV0j=3KADz3J_s<&7UNMe6V=Q|| z8GFbWXo#!A>?wwZFt(5MP4V8u%6nKJ(w`>JA(szXp6}WmY8}lX-F-*si_M&8vYm-+ z5g(Xk$MNm@6Le>7weA<_Y5$S`4j-6n|DOM%gZKC^fhHdp+xG0}@HTim=>6tK=!RFg zw{Qf0epUSc3f;|J?Ac@aS>)-t^M{)P34W(J{NZeTq5_E6-;&HWXQr%A&VOQ%L*2 z;%upVc4&A#;o4*YMgK3x%9yy zb$yIo1!rb$_0_Qlz`d1bWna9XutGj(K#VM{{$JF=6`eJuB2OJizYe~D0F^}PM8o^#&be87c+2gQ9CU1iH9;%aaRi}8# zSqIeFgF1WK<0)qpG^oc*eg)mp$DiK(8Dmq*-civB8kG6jS9}zHNZUAryHNf!Gj44O zLcfaU&BqVK3QX#qH)}x4#n7bN@Xeb|o=V2{Vqf~(qNR_+C#jNj$9Yng292V442E1x=byJ@QBTKE87U)1n*8$=|-NHGHc} z3CxEkHuz3vJ=NkvC7MQbvf8i~eA0ZGZx0|m5&yz@)RD&6$fr(ptM|VzzH|l9=|Sl5 zNe@iw-T$U}Pb>XJr#xct(*Gv#8sW=q&c}~0U$i{Fi665s$WBoG?)72H9^@0ydv%|{ zo4GvsrO4^UXTR3m7B`AGvxcGD%-n-+`|CYzbFr4aV*_EyL^U64G(V>VVE=B-ERp^x^QUo+OXa2$Q?<^48m`9<=|$3gp}=DBp%&k|Qc+}r4tgU49z&nZK8 zJBPt(x$K^e{}Fc2C$p}TdCJGg+n2l!@54GGEZdu7DQhNq&0Uy>-sip%k7?x1gMSh~ zFTVeK=w=pxM+WBSq-Q_bbA|YZEPEI5HZLB$a>#Ql;m_SXIs=qv0&-FDmObuq-k$R6J&yFF zNwe8auePNUr!oJR8&~X>f#A}VrTKaaeBl*@-*?l<&PZi$GW&TC+oB%rt(r@USDpsS z)fuo}X;^oA?>;b%dmgMzXT7kF@IM+C`3$CZ zwP!!>mZkgI9(BJx0&E+-J#h| zgmz1ZhRcAK%Y^TP_p3gT@7v9p^Xf8BA7s_T^FjNc6nd!?{SCjZe#_{CuKoc$pMSiu zy$Yrl*Vn@HEp_?5I{3Y1^g$zgpz;jV+XkeqISC&bXcgIHt#Hdb+5P76e3j77#)fAu zwpYurp^?13#&Y#RKEv~E!=_mLe1torYsK@yKZaeNFAUE&N_2tyW=$BM0P$C{9bTcj zS$i+Lb$3jvTYO&wbvt^fq`F)AKA++H-abOz*ez{8=*=4HI{_YoJMpn6f(C`ZZOCg+ zySMqsMAP73$df`I_1j9(cjU>3f2sBVtti6V8M48O7FhA!(6`ti9cJHhY>0SZ+TUpS?bi0c*J>+M>e#)_KgRa|g*`2~{k1*Xx8Hjc zCbNxS2G2F^@--ZkudUx|(jDKTHr~bY|JXlk9P@oF^S+GvKZbR16}-!pVb+D=T~^aC zk#(}cOSfO*PL4={qjOrW|J4rPQP2Oq^Dhj?(np7PF1`r4uWa-jnfX@oeZpRGA##K$ zw4AZkgZ}a6sNNBmuTSl=lnwo{cQCZa7>~|Pw_oP&BiW)?n0+(Heo&7%hw;8LbiO8)5-p@QR`stdG!&-XeoVGaTV`tPrD%80zam;&}#ofIkm{6Rew)- zhhD~(&*2?DMOM!o+wl&x=P2ZLXH#}L^I=lbe3&HpODq58*wdvRVLmK{P80ue0Pg{g z7q=VNM&9i7<~I8MSQB$wdbuOb?T5$s7ES@zL-HPstNO4O9^`uUX;PnGqi|B6k5xFS z&#xr>#>Z9|ed2q=3fk-tS7V2${cRfU(i+XB9m#9d_i0-@zRemf8{^PN$I=HEkXPr~ zvE&^>UY%#hlD8jub)I>2;z@)>A4%V(^|5DsV$ltJe}{N4ZU=GK5ckKVxKD{gQJb;W zjeE_dAGY7=$XJzEefDdHPY)6~)Nclj({Al?z?bRM3t4Bae=@FgD$@JA_}Kf?Ro^C0~7Li$0+hg!M*- zY)x*aZ;Ee|Jb9e4HIY17wkDD%%dY$1w7;{>-&XBDMXB%{YY)!m3>9)?2 zY;K&f>s~5blPSJXEqmHh?P;kU9od{`*Udh^mby2UGmh+=5qy4)tqJ3nY-^HCTZ|5H zPJ>4dG*{c~muyYOT;<4T7x5-wWMEMG+ZBNh$l}<; z`=8CZf)A60e(DD9qY4C(2Xy;UZ;N-ytJt%J$NS?VNt_4ber2O~uzntG+lz*6;|MzpR3Uv7Ooq_GXS4wJQ}kKJ2H({s zbHCn>T;illmRza|8I1ITeOz7g471Pnw0}a`t$P{$`vhTEm;Bzry5vLCQcVR{O!w%i zz>dTU&O{@Vtlf^Rh&IUve*^QAyOOeV8Drg9=Z#UyPUJS?W3$`rO3ukJ<7@m7G`=&y zN&BAeB(qMpR>*cl`6X-1a>jRoGro?^$OEL8&4?#!yZ^)k%e#s1nlHN~=QvH}apyDT zNnW@QS}ym1GBsy%?OVvBd*tHq(5>ZmJFwi#NwW*tV+7f61iOnUb*S8G%I%Rl1IkT=W2iS%D?*f1Ir((@{u>nW+PZg9oXi0bqM#Iq&oKXQTdWR=Gf=#KgG;|^{&)#J`^Q)3QzRKO>wdWU$hxGhFsvW(_0vawK2$ z?36TyYoYDFPke$oqxtL2nK_>)${tWW<1Sh#dE6&6{~yyg#!TNd2FYVK2fvb^>pQ+q zDpz#|R5s&kAah7{2@mDfm{pTUW8}QIlWlkA9k|uY?hP{!;;%H{7uKD*OR;y9++zv- z)reeqgq6`8hky8-GUgo5H{B`vYautsT#>x9E;A-Eo$J%s8L23OWRklR9DDulE^u z1pboy>VD0Abq~J3<+5M#dz0TM{2u)0jkhi6kaNSY{&~}FF96^C=gqfm1b*_*TW;G4 zEGgt}q{1KYyNMt7XL09ON#ULRZsB)V?*RT$k`I4IJqhmd%VRA>Q@XzWSl6!empv40 znSveB6xW_30G~67JKJd0r%3jlH^tlmwv0Or zC-bcs8NB#9A2vyAiCfAqpLj2AS7gxZxbv_zE$cyPf;fdk3M)P?M?egud;9D zo94-O=!ygAleL~)`5^jafh&i7PqsZLQ-wfqJ`#W=^{J)c`-@m+Q7oIL-6gBYPQee#phm*voL1 zRuNt-+ZpEKkN9tB4d(7Fr5j_~x>7bT=Mj#K5615iB&>Hl56iD~?|anzN+v!~$^6RR z%PH5NiX1cx`A;@-pd92uxyXfjAP4P<{Y$3=Z@r{m7G1bNwUzT^l#&Nj;%?w%1K|5>)O7IaN;++@tz#C+>^becESfW zMRL&AyKlDm{YDO2hh4b(GE)5~M-DpiAUWuJo*cA)jzg!<1Q(;LFKxSbXI)a=l7mL5 z+sFZv>-OZJ)cp+Wcis`|Mh<%L+uii%pV=#tZ5SH3FFN3Z$LS8e&@pJG6HL1%w;nQ!5D{Z_{7$b%X$bo%`| z^G4+%WT0(rPxAfs8y@kZV{H46+qJ!XeEYWda&2FdWT3RavR%H)gYr3YPW9QS-1ah1 z&wgV9bNyQ8{58z|t62l%k%5lGek0Y$K;^4ZTk6NABFnxLxp?gs*;l07x9fkk{`a!K z&wtTfuT7ZgVmJ??$)P0xct(RYiwaD9t(#^wWPY(7}uvLW7g33T))qr+dZAA04==HpzId4aV;{7S}+ zR=8)9&Byc5U6R{=3ans{K58Fq##`r&>@u<_KiLlBM1{F`%$!G@rx%k66Y>oc_t&w2NM5~T~*DQekdLW4xk-Td&b4YcqPw6#ZdO^|8Iu|@%=jZ!o zZ(4%iMj`iy?SbxHjIMkf{reyIYj1wUbCBU7JHl>mMRNR3#rLz~we8|RQ#`Wda^mCA z=X*G-B)`*KusQRR_m$$0+vbANw;$!cvh`Bv8s>uNe?zl_hk@W}AoZ(HwqPeA`9{;S z^TW`q#-4Z^vMkZu7Z+He?_fLM6)5|N6zJ70x;Oj%Ek6oYl=sdD>fV3P7M^>YjVnp9eJ#I#@jj*>$fLWv z&41EyXZ8zwp&@IzL#>j#sL%ChG|%II&6mL^q(gZIoMk7XJMV+g;<=RdNax@aX}->H zHxJ}ZA`S1?Pe-Ud8T}D}Zy!0N!0eJOm!%Ma|!fSHZ z+4Tk|aJd=YM>y@^mk&Pq;FAw-`QVffKKbCI`>ylhagy=UJ>c6Yd$fyJU+_}?fQy%r zmwg%B)W`2Tpx;Vd{L&hK_=K5j^jR1BY%=l9;8xmwcKuD@@-V*(i`PCcyiV*SeoMCR z;aZ7`=a-&@#W0-VO!r9zD+#W zXT7O!Xy9=2>Rm>sb-Y~PUMQ6!o^3)*zNZ}stO0|J`HwRxnwQG(0d<%cTd@*qiS=c}Z@CV7Y=MpbFAroJo zWzxw7I9f{$l4ov@x971Nm;+ZaHl~kvQr99JxeK`*)-j&+LU)6Be)Rg;x^p zkraM`a28>mXgh!`P&_nZ!wkc!e9MHmH2@^%q~rz6iZH8hD2r|GrsYx%R!&92+o=mC}ng z8*(LlGPc2^;3Y02z4uM)A&Py1r@s|`*HN$JFlSM(mu@g)enwJ0h0k*9J>P|=bej0IiW51?&*|6;$Y5ZJ=sBd@iUvf6dVG2m6GHdoHpzX)S`Y24< z3OBKrJLA#I{)9ZryVbxfdkau@YmFDXx_Z9-+x_-Wpx1tl;onSrru`0gZTyw=hn3fC zC_HL;O?cda@|yW7_gnfa=^kN6USsUm7Ubs@w8?8q9lJG~`w~2PjpQ8iRf-_5N%mEO z?#yUp|JFQw;D=87QQYq=`Gf4I6o0Q9f3UoUH^a6VyS3%5c5Bs+-C7a$_EFA*NcHcr z`^9dJIgsqD1kbnJlh@44Z?ju7@*3uq=bt}5scy+@8feEFqwh$n+mqK&_cP3yla5e# z8Feo-{z@5b_E)rX5_@8@ye8Pcz0AUqJ=7q3h`REcGV-X8B~wuUT4|&bw7x@-Wmff2Ao%QJ5cbuKn{zlqwVI!dXjw7i_?RW3z-9r1<(*A#8 zTm4Oaf4KJNw`+fGLHqW5c54UrTY9^E4F~0G>$dHikTcJ1pqo(zY31o>!437Tsg z^hz_bh);!*jX$kRvx|-wO)s7!h(AREyu$hXrohA8=)x(`dJpoyI^g*B ziXRl+{AcLjQuvBx`07fw{suldb@&iOn2VClJUI$|=+4scUiNXXt&+`b;@*pfET@f` zKXCN&%zdN#_H61dVg4h_G`fQ}S(YaKE)P9{yg%n zHG2~0U-DY@{X5aR?u=}Ubtk>zk=ES{qkRh>q)ff1w)^%@;oav!qk`K$=9KIrbiRlN zjW8B-X@9F7Hse#R@EA{rm21yd_(Ct7W6vTinsut*p;@OgZx=(eK7;(|kT!hGan&U` z)%VGpTz7@aM&@mBq3#&HWRV^JE?h+)6|5^|JR> zIJzn2Ro}lxd4;6&%8HUtwB`RvDr*aIqAgdual+4}hfY~>zG#_D`$eGq7!)r(qu@~1 zlW26W>2-0#@{5&-gs5(++%NDLtLif zy!m%A_J73f<^Ck4jWGWo24DJQC~-r3QvX2jmHSKmTI@M%dTQ8Nn zE*0Cr9Bcz~(KYa9)Wn|X9(tjJI0=1QZ~P8Y574*iUXIuO{tbzX;hl6B6*@QU4R{~p zl+a64T>HRvj=%XZ-olYxUzEPAoLhX*J@#MI7e?PkzT;-OY4b$CVIw#K`i=NbZhU+C ztv2=C`e?P$x3&73F9Wa1jM+Qb`pW(+a(aV#pQDMff6Mhdc+2rSSm4obX{R{!+lSz? zTJ@GX^+snp_!zo~bJaT!4yf0q-Bhn=x8EG0-Z6*rHK&bVLO&(z{qv@@r(GO>VfmWZ zWIDEtTgjuoeF>a}pTtv{os%Dg?288iL;{>jD$RQ+8-y(iM& zk@47oGSAn@29$ZOd-5Ax8&LSw_~GW<*miwaIq~TF{K9tmY7ffS*5|xk+RjJaqnB@B zf4QE0<~sJ93G6%9qT{@Vx7e=6Hqp~_Vv|b0mhYBL>ZINotk2~6r}+p?*6OFOJ`qj! zXKZ}GY3(;clNsCAQoo_c^Ld9NvP}PT?cV&at(I=Jr`?nPoC_P9{&Q(KItv+KzfR%0 zzuXy?eei5-yYi9c?wcLte~;$;Zs=0cf$c({B)w4?y3|qVQY$WWX>abL30%nA^vFn{ zNf(IzmQBZZZ;^cvwk*&@wa7|ze_H$tUs&h)E$FGH(Pqt+ZS2#@`$N?~(0{pWJq*3o z#9EIY#acgwIlIKsg&G`cp&7b@L*8gd-W`C}4}`n4`M__$2mZavqL)ag^Y#_sNN_6L z(`Gl?u)ED}w3In5`zg_lde?O#y0j=dHE8v+C;J*+!u9RWftN_P_iJr%KUrYfpjcbK z^suFint2@SN#CG5y>GHd%ICM5e9|pe2GL0p{-j?pKGC1r-2Y_mC7R+7tIxKijtt9Q z^S}JI!oPnE91TCZ@-O#>S8cg3{04IIrQoIh-o>8h)uC}&Px+F+1mOi7`Cx|q%aM-F zw`|IwV~0itV*evNkUK30uKddo%EdOAc`4iAJ1&3^8FNv1FMOI;w)l`2NNWwTR>iZkE8>Ar6EaBzbLA0L}fJ?32k(Q>qZ zF5&2X9u1ypS1UZt3uoA~g%{%%aB1#MuCM;(@b&%a<3fJC=N9++di}0M*&o17_KvdU zh5#0?=#H;!do%2ZC{tsIy>Ptl*1<9Cs8-01*6WYk_%1%Bq19JE*S=^Z_P*39o~Et5 zp4c4g?z0U02X2`a(+0<)jC*H%+b8;ZG&dED4DX`sIp|m|-cyfY!`R2dzx`CJtP%aJ z?s*zXpQd2*SHREc&3IQhKTy%T6y20;3X9K~)O*sbJfn}AD4i0z1@YY1ux3ZN@4gpP zzwCxBY`nF0Jsds!M!EO}z|Z9A5{wTf&Km6ab9w32mf>#NLee&;vX{_~M%q-OGXnpA zYu3P)NwcQ4IlG2|G`%kG`||w+uj+P#i4p-9%d_`a3u0p|^f!=2UdYlY4h! zf66m=W%X!&rVo7r?|d_T^^w1DD7eKDa--kumf4eXW%P+ zoZ^q8?TX(*+Ajg)UlfO>XS>VrOZfj0r*%?SogRC2 z+S%cnuM^7yRi}lT*emM#4L1AIQqC#)4S4=8dEBihdiPb{5FgJx+gqZ$h_a1r?=AKj z?PX`XGz;%$4VAsVXruJBU|jp*{9@-G#el{9pf3UzXF>U!M7OL8?v0h-NL2RUQ_n{?mO&Yr+$A~U zgTYzcGsZr0*XI1tJ$zUBwJ9S$2$Tnh)$m@T=+$z@LHc;_Tb0G$aYdKVn(YqHr1a{` zK)J

EzE2P#5jg-EO2W^Wx8T@9Q;n;D?hjW2kfwA>G+2Sz=w1ZTw5KxvzybCGU+v z#!Y);Wc{lAh1yB~RzOqLxc_VT|I(%*;UIYa?nd9j+T%I1=(kFI zJ#}XUbXc7AV{CiloW05Pdhx2ZGrS$WzFcw(Km8}UMF*gfTX3$3ULSmcZ{cs~x6Pc@ zCFQx$EjjjV;=*2>l^c@pi1}Wd6O#Ro`5(+7E%&LA_J4>w4E^wpb!U?Iaq@aJiewI* ziF<@Nk9HY$jAeg)zHj0E$C&pE@=hgh44Ij5)4EVwR}n9tQ^kq@lrI@N_~ll^dAu`0 z|5nPDl>V*k7TQez`sm+(l4l%w_M!hOQ<=c4fJ6+2bTTZ7lf4sOd;$Elx0dCx5{MR=RD&NJVk<2MSDc?xqBy;NJ#z~H1 z%4Gk}4mvWYOyIBFc=l`OJLL@}?-IgYlgc}bxNC?DB*mRZoaTc+DXx&X%Zbz6)flPH zn+SXD$>V#C!hBbp_VN;J2djzOk(8%ADNj0i&L!OB<{6vx?Kr;4&vmo=P5qHdTpDrz za^nJS`nEpkX@4+6eUNLvZ(ydq5jfdRSM0*SnlmiJ-p@OZ+B3ZN2fDBm zU3$z0w@&TjUiveW{tsduN|)<>Q+RMvSbK9}2hoH%t#5hBF47sho_3v!_pe>Ne+g86 z=X1EZ}C5wX8(i2)LWtUa(1sFoO~AiPTxH`8NP2d;dJnB zJr8c@P7LD4;18^L>5+c~UYod6QTpCl{FjVxhBrp^`@JsycLUqTCDZ^w2MW6iaZ>PL(2 z>J?ixgtIRlp24x_O1CG|PohUE;TN>We?&hI1}=p+5d9&Zaw2I=yNEjne$TXv@jjdX zJ2#yW7A~^+uLHN8bDCGgUsiPKIX~mG2R21N z@x53$DDq|Wx7L2yM&sLB)(e^){>8;F!#-Daqel~cAvz#TpKe2zFy9}TU!EV_KdEX> zQGQib(f2ch@$Xcn#J-4m-;G)PwLt*d}-Ip z;n=9{e?H$gEKuwVU4+i83R+dPbUHjz9(`n0=7+MOiz1@2fYEe(PAw~b`ecWX4Uq1Q zWoLxn_~Oj4@YQ_qfuHU=^Yg94K;D@(HnvyBR##sU>+cV|J;K+sxd0yGW&f42G`{Ct zleYM!+tIZ#X9C4msHxN!+Q!@n4?HJaH)U}6wcF1Q8(gdcXwV>bH7+iUpHkxcuON${M=DFU=<&qrC|iYbA` z@nzY}3(8*BF;vX^NtzGl&0prjDAo|Xpzft%pGvn^(x=hc?*367jK84pd^h|!W2x{W zH@p}>4~3Uezpwa3?|sWg_%zv1nmq`9sa9!*y5aMJ@t-SP>V_}G$6Ddb-7tDHlWN*dD-v0P~ zz0DPQHW+_E`N6M{yZ!$GA7n9J@-diF;`oIroWc0iRr@!Lwyfp-ujKCYenZ1i_MN(Z z=g7Ztd!VsfXf$Iaz9*bG+svCf=6NIE^2@s;+w^bPN_pB34Q;~uy_m3Q1np(;*ehPI zzA7di_4%APKcCVu#AV4ls<4~9nmlH|ga4opS2C~6`}LB0Tng?A>kLzxVf=T_f(&~k z-?9knY~DsXM*F&#e-Qu0bE%#T{xgqAH$FCqbP5Z9m8E^iqy=ZC zl}<_LaxHd%g``zjG>Xznme!X33X@*?B=NbmthZjIS6Fns(huc-a-Qg2?zr9;jCUt( z4&R6HQ(ODHdF1!rKHpx(Qsrg3X}mmMSag-XYkwHWnSZ!7l*Jkvz#8(_#CF0uGg25Q z^{@J0cijB=!o(usrnad4>M!-JH#a5UiuP3Jm%5Lh4-OsZN zL;dT^;Lp*qL3c^d_Fg`3*0t){=;Mdkzb9Vr$P)tQUgtkxCsoNk9B%@viVD@=DOUXN z^q<~EG4$NsuC9%Houxl&WDoN0_88!P6OZm=|F6VmNj}=rd)0wUzSeK8^H!fHbKj27 zUZw8QKnEdO%AEhMzO4y;TOoLR`nD$5Ug`z#F?WFs;(bSGOySifsqQuGk+sylw0+&K z4Te{@d`Z8l?z5@;pnJZkD-(JvS*D(ML3{m2+kI6PeH~w|2f^9sCfTcBc5(iI8SCget86(hk)}3#>Ok@ZFcI5axNz0TwdHB=R>^BPQHJ% z%U5|&KBukg<(J0ztVh1VzK%T?bA$8f6}PR+lG?fkTN%%u>wvbl(}_+!LR*K@*1e{! zj?Amcl{3}1!DjX{RHeue#eG9bPA(riIrXcVKi(Vm%%8Q)pNu2) zsd%DminPCF+2ak&w8t8lVUGbK2R}fUraDbM-W&GRBmLsO%f5MS{hss0)0w9*&r2Gt zkk;`QaMYbol6SpId!yLHR`zuF2lQ1@_6D;r-D-u!mwwmamSca%z$|-!fthxpff@Fx zz+7-v+5Os$)6#z-N2BaY^rX^-h0&eXRd7kBQ#ixqNLinFeDt!&3XslWu; z4||@0_+A*udpibZ+0_PS+EoUkHv``3_D7}LACij;zs5uK$Eu^~kJspre^aK`i1@s^ zb=c4hwN^Jm`%miV^h58P+WNug?i=1%xAsMb&)mnUzR`Nc-HX0+AnqT^R=MAuVcjpLF^vA&3QI59)DGuMU7T|bEnP7?@#qH89=o0{k(yh5`1oz*D|ZlQ#za?qe?c43E<^G`PAc-CEtU zB_$-=-6%Rj(WdY{#xLHv?zzq9N1G)bXn8&`S{NHuU8Q?<6@D<5&o?mBE-{e%+YC&%zX$x>tv|WkTitSp zm~=ht!3O5qXBn7dpJ8B@{aphy?e7?vVGl4c-7WC6Y%ej7jX=2ek zgnKae)ECDxhFwhExpta?Id-anS$0PQGi|?t8Me>BbUQK3TKL-}916hUIl^(~l5l80 zUMIR~B@feg@%(~1dp@Ns&7*BV?VBc^z2jpOpJV^mK>YI!%(Np0X4wBWFx~zq@KHCP z`ojX38p0pAxOj8susBfn+or5s`z>G_4mtK8OnjET&cIB2je!~V8wRG^zXMKBs=Kiy zv1lscRc_s}|m|@>zV7h%5u)s~PG8a*%+Bn@U)7xi6yH~hn-0YTdqg%#g zpf`q+Y2M&|zs~)BEwFW7Ay*sc#+L!x_N#RJ3KO4gpU*uhms0=X++|!lFtn3c1G;K>8QSI+VWrA}3uTGNf{N^GU}4s>4CFaAIATBl2#@wl=B^={WA0Qyklk zj$`MTQDMnX66iUWqRWB?SRNRSZ;ll(_BeZ7yf-gP2~9&zBb-NJKkms1B6GPnsJcT4 z{%d*u`3}vqg?5Wxdm7(5$&w?NJG@g9a^$8Yxe7kS_y}j%A0QXaE9CynT3_e}?9MbN zzJfQ~^B?48;2h|R4D+hM;$369gm)Gk7hdu2`|%CBf0N{Iy6a^L_7sKKW9)^FfuC4W z==un$?2gQh2=Yu%M!UJlx9~j9EaZ9Ehv0Wo-7VZ$eL;O=HSM@B7;h0BgUnq1MnUeJ zLs!RBK$RPJ(+^p}lu*^QV0c~uda8aMfF;mWDZtV&^px>=sVNBV|JlXC)eCRGAiQXq zFZ5I7Ly>~C(7rBKd^m3#*RGQepojg^J^U}_Kl|o#{a=SJ3wdC@iRVA^L!18*`nEAU z7=M=kQOdEQ_cxstJ*NUcFx=)qkBpZ#(xH`rfy`CKjHs~R7bxR zK)+>lQ2wXTg^6Fya`O70ENY^xusainFRK6V+D^mk8@pUmzhzp(@U_Q}sBh@>y?Wu)kg}-g{oAbY z-#V+Uf2GcGvA43Y5r&3VTW#9efbL7Wi9em%A-w)9YybLZe8cWyZher_A@p;`KzEsG zJoCBFa4-5gy&0>uskabyMreF@KF;`&F5v6+c3^+$rtsh9ZJhUuSH65}-oL(__qV1m zv8mda-al%(H#W*H*dqv|xM!_XOqXzI1=6DEc^k>_0Kq zk{XO7D_+rXe8cedo%L>M!|=DVm~ZHPnr2&Z`BuijE5Ev`XiMsa^;=SpuV2$QxPSA| z%Hiw#77yFZxa+J?n#4y|Jn@MmhtOL$@Dbbdo}#~JRSs|b%Gw{@k~(bFM|Xq=%inTN ze&|W$Jz5Xto{kNkTROIJyvHe9QKMrk7!-bk@!K=kihtm@R%m|uXt&O%kY!4Fybwtdr146nZpeOp0# z*sK%EzKM6)C4+X>=K7*YPxPJZ{2NM#qpxL*BGB9%W zGmUTBhO{%S_yTygG47j??@*_79npdgA?Sqdt@>_)xOFL^8seTL?oKyO=ajx_p6r7TI8^`XE%3iwfPW@^Qce0PZeH#AT1Ts%(K><_ zgx1shp;|{{POw&}tVievbRQYx+&sEpLF-p~AL)~%ACjI(yo2!2H>FAbE}H64-{c47 z#e3m)3OMfkKy-d0*-E>#R#u6ue zQnX*Oqdy&39KV7%>6z{}I@+H0C4{A4i?H`*549E&KP1AQoWcLG;=4Hyy7NB^-O_am zb8d7coK5&|$STvodt3tj5obs$@x2nML&q!3S>q@CiXVTkE}X6W7k&TrdCnNC|Guf+ zr-GN-?cv#%u;%segZLVNhuR8_(W)y`JJqgt8B^`A5%#UU?3WSd^{gV!Gpz&KoV*T1 z`@YAV`MZmgx8FWx<_WYQ^JEwE#oKS|xNjiBew!eVY+v6d-(l&%JNh3r9k_(GnoI{) zQEtoaucB}Ep?6L}|LjK(-2vMc>|07@+j5`|ERGH=G20*B%epZ%VEJhw?=70G>nuY9 zF6Ax)=G7}kmNMt)AL*sknL(XjkhT)qQ@Xgw+>Z5iTT-NpLm%kq;k@iZPupf*Rxt_%0AEwqABk`fS$~;@1Q=Nmu)mhcDQu}yYXPT=^n9#+{7409~tG{v? z?UdgEcd2j{9q&f+8_LW>W^o8_gz}Pn*oS*cqg5Sw-<&&RP^`kWZ!Ux;()_LI*lkEYd(wyiHWKI| z^P$DoaKD@SQ1^pq&c4o_(=VZ4DC}en&1XOKx!)g}VjcMA4>H-83WkI=$1H4?YOBww zSHH^csfv4(bzb>7x0kbDsQlXRIpu4u?qYAPI6-S;JTxRQK!ne9xuq9HR?z|sIxf!JO;z~#}gnJ<+-_zK?j*Y$W)Hxakk38R+ zBAfg{UsX|mUrB7VF9rHNGxmTv7kb!lGM07y?g^VTY8SMna5nPfXN4E|P2kQ8+Mv0i zduCqhYlXx!-!;by{a^0hJwB@HT>M^pCYL?Akr2WqATtRfnFPUbOH$EH64a1rl#o+f zZJPk6H5V?8wJnH9LQn$($e`6a*e0NsOc1LUEA+JGRxKK(V%nZxPtQ3dTx62r7DP#` zF~9G#XC@4Yw&(JG-uLtVu|Ioe?R9z9v!3;==eE{@HjFj5a#eY6BeF&Xbo2^!%3a=l z;9hXlVziaix}9|IFC4pDB3WOUx1EOG8M))b=QLL*eNX8~a#c|Gu?FHMpm{6%Fe?W2 zQ_~LNWg^E4jR+qT{u1<|zr`HTwNU?~-!};lMTflwzB&v3S^%HThu`KA-}7dR#P-No z-Hj{|dcV^?6q`tiO2>YotVueby^4>paj%Dvz961A8bd56I{$L$^=@0xrMEaq;A?b%Kd+#}- z={0AbZc08op(*)@Hl*eZa`2%QE8Ars$Rx%BEpM1#rD)b<4;ni}-MazfZ^N0^1 zhd$&qq_}QGFOqe*!kqi(k;Jcwo^U&7MM!Lolo@y~!ZSd;Fo}{{Mw}|LFqs zd~~iR&Vj5a4)Ri88EbU&IQp7epq|_V9sN>tnFjM=fvaUgj;9Zv+RM{o8n*pU!*%ED zcV~M(A-2rH`uZ2co zs62_XXXIYS4ElKy9*mFbzkFtE=$Qi^>X&n!h`T(uocpOnUJLTGAEV=>L0c^Y(>%XA zJKgh%@DS4|A8TV-V*f4eg4dHd15ZmoMUIo$x7QlVc8LtT`_AwCWd!YQ&eN&82|0hK ztXJ!4?_=;Ov15H7KisaHha391;(R~JcM;k&k`ubgq;2LzY_05pE`&Ka=^dD)+xV}xEHwNUezzU$}hMt zuZ}fUVtIu|UPGr6y57#2HC^=K?YJbT6F4S5;^=6O&^Z5wbx;1+Lrcp7_?Qh`Qa5{Vxl=>+y^fwJKHOV@OMJMqc#9oNLvFh} z+rLKx9&#?AFK-O$%OCJf=u2ps_3J%Dr`$HpAM=Q84) zVjh`OEz~n1(5C3sar_q?eG6Dpg0MiB(?}QlvW@=h@DS;<4j&mwEQHZk{Sb6pXot3h zr|gmTf^DfNm+<=a)Dc4+Mt;tTxLSUh>syYQ4sRv@k7oGwU5Q@~rAxt)r2X2!DSI&) zKj9gYCgop)j&BV7Z>FxJ=q9E7-$nb9N8%m@-$s6T$VvM8b@to}y>?QU#MM5>`^7+h z(MKh(_z&eh2KxAzIVt(aF(3ZSoPUc!ySr`A4bTe%T+y>0d&&sJH_a(M|gS_7+?*Q#y zxHA%+N%!=dk+pzF?&@NEkqx3;hPxXws+U4-7 z(SCUE8sUfct{H>yu7=L1T>)?NCj(^$T#_#UR|EN~f^hvUGRtVMK0AP)nU}X`8{==U z!|HFZ?oxZdqn?phv}d~*XixG5+B-nLI|A)_FBA5|d+*shuV}B%>Tge7<8NU3mQrx9~H$-tAuR;>GmA4f-_SzYK3daaNc3h1>m5JOQJr9AUR zW((B$suO9UrT>vR=pO{i*rp7YnG;gxVDF_e6@fC9#{*^5)WI^dLdv8;kHP-T z4U{=}G*G7FQkkrfGL0WyDsyX~Oj>84%-(5(?WBj4@qT!zOirLop_=IMiFt_dkq zc>YqEiGecS{|J-`%tIOP_>eL!&|k1W)MMg~{yn5j z8uS<};|r8I_@_Xbj!R|!5K^Ymd#TKS2Fj%UF;IrRt-{|0Z~H^ac%iLeJBI^h8s86; zX`VS)=CzPAg(ohR`CXul_q{-wx|;^ew1kvtId-Yc>wz+be~8Rl$vW^bve&=?;vetU zSr06%V^QpDGr4r;w_YFPdJDP7yUmCf->BE8xGcy%at5;8Gop}z>W1{b9>rPV!8Cl_ zeLDL(r?Sq;Uh@ag$7Q_?+GrZFc^xRyAMmWF$G zrRgdwje3kbc;R=yC!d2eNIF?#EtCbu_w;~mOvg56B5yvllZ&3P54lrfAA)Vq!{;Ej zp14l``YrItTIjd+_~8>Wb5}H1pZel&}prK z`vXE@{3$Tnh>Hw?@tmFz?OGd6TYeZ{&aa3yVBCTZC32jUPuh{+_nKL~CjFN)z@-1! zV`g|FAM$J+x56WRqTaHu$IPCi&xLtB$T+#^K^k=k?nM@mvcH;%U*fUlo~;ev=oNiI z?jY-AFXB=1)YB)?Q$-JHF@K{siLq>9d<@wgS`hz)>?xXdv*0MHwSRdLG%*{yMWF{8 zG310_5bG;37(w}b$#l(-k=@vU@MX0gqrHv=B8y0y=GGkgB0fmb-E6=rXDEs+(qUdb zQ}%!wwuocc67toPe_?G<$W7{jEb)t-%XiFl)koHqehnEIIh(%t;fn!2kqZygw#=Ud zd@(kBLK^u5hFpj0X+dVnRVH7h$WFkL{ZHDkfd+NBf4Sxf-l>pl;~f6FMz_>a!nk?S zx3CM;Dy_-gzdR4yurm65jg&QO%n{C|xzofPF=Hn&w_c#H<+PPXzsgmj&r4f^zy9R~ zFQNND=6%Cuqq%lCL9|5H6F?|w7wz#CG@H;(68p8p&V z?~TlR)k+KobFr*{`98z`^^0l9`{lm|SC??xPg@@{W*>oHk*DV)_qUiFIo98(+Gb-s z1s{p6=r2#!an4_&-ut@Fo=f2qS+o^|amB#E(+=jM;9kzbkbeJ~elK%Cb9J%@FuXHWzL8V$`31hZAl*-14D-gZ?#&j;M@joMEA@M^Up;+ zY;TTL(=yxmmK)C(!#aOclZrGGS2>S%8l{sc6z*H>FmdxrQBUn(}6v%m-d70 z9W{6j{tuOl&2wLi5C6f(cmD?Z@zY~}gMVCQOq1ZXIy9ndN#MieYZ$~wl2iIu3NBg# zxbVZ_r^Ao$J}`)jkB#>H<9Z$ama+N~I5`7MSI$>IP04&P)o!J&2gp~+cr=Nvg1!pP z2)(_eDe6gTtz>Mps%hDcjNN2#=AEis+%dU3SJ`Th3Z68_tM-Q?=Gw!Pr?Brjrwm+; z5ZpQ(wfWSqYb*I4GuLhzZG5-HvloFZqK4EzmqB6CZ;5##uIULk$< z(;o3kQ?VTfad&|Gl`M-$-b zlaagleKDr@^%2b7;M^1%{WEu8%Ni~;aIpbij4r!}v)hD6$-3dUxpE#U@LX7s@43j@ z5er^r-4dVn5B|&0~S+5F7m-YH$`=xrk*v`ebREG~q=t1r)-b0(3ImziYX>Faf zCuilh$e7dKe9Av9ZAIpL8cmvW*9i1Wi^}Qif4Y?YEz73ID3^De!}HN|CQlb@m#m$Q z+xm?amTLm6uOrvwO*DZU_x0wT=fL*k51s<1eYz1pcHi@*h8{NA9xf6#dvCaoU&6QJU-MO6A%Y z1%1+<$UEO>42?0ver${xYxsHYE0r6P7dcbpOIfdw3D4>CG{Z*hodDlMb{iOAze|w5h%FWR>ZQEQz0i3l^HlVd zX5!c^*r}uXR<@UO-_-0!8r@OUlZy{$TK!u0PE+seLPIUgfhyLJt|7{>FQ!4ua%P1= zzYF}fMPjLtQysghx7NlS5qbFm_7e9m_tS8YepzqczX_N+9&>nZ6xc#!Qjr0IJfRSt z;HBL{WC3a40nB2I@HB6Ab+#U%rg1iiYmX_)CwB4cfZaP5pFz)xO}fT;rL1|~2h^)kwz~6r%Iwzgc$=oWoZ$B!uYhP?%^6I@7JKHA!j94u{F z;d#=Q7k@up zWVcJQ!zEc%ol}Z)ho*4u4zjMF7qa&t(;usJWh{}S=K$FKZ#wdAdP%3YvuIp2Uyd@$Jzzno@9NMI)z@wf`141zyx9VDKJPZoG#;V z1r2`_Sc5dIMCK40HYrcAeyK0iUr*eX!IPgwzm@t$-c{5q`lrx$18oT}7I{YSChs3X zuipc1;lpxosnE?`yv0`#^b?4_*FA*T2IjAviy0NGhDJx!l}5*^sWJVt+VA+LmQZe2 zwbhyHlhdng*->(rTOM?6j%i)R*jvL)Jx7Heu)Wxk8OLjS8+E!&=+F5N+~f+=lv8wn z(bWogHZXU^<{z~8qbIXBc%)0CY#U=ZU^X4@Ee~TI3THjyY@_A)G!AHn9_BT%9xco7 z?-;H6E2)bZ?VJHKvAXb~j!2WIFdjLcIZ;L2?`+Npsb-BAewM*mmo+_(O`OB)`YLO0 z`iTeI?~b*!jhv@k`^keWw6lYF^L@l_=&>Td=Ki@kN@eK_968d4J|W-E8NYVRb1OX_ z)?l%BhFRCkIUO4e<$19cHYWoh_UZ!YqY*h55Kl=a*WtfAk;hpU^*d!?Svkb0^D z^@y+MQaR0|LmSeTN^y8t&xun9KH}8=oUdFXk15y1qsSb617)KqSBV@^2~A4Be@U4t z`lr)>{j_7#F#4ZG|Br_Bxih5C%FLOPsh8S7r_H)Z8?W)~A&xhX@velPL{~Wg+!E6| zjD82xMBk{C_7a9UJ0|3NG>gNN7QknP?8N{NLa)#*dcbXr@2!mUEy(w?uuu5yIzjnP z>?gLce8zuY%W~194rtu_{Ke;AeP|9xM-M}HGPiyV&)kR2N}uM~&G>ApMrY12HJ9dU zs`iK4+GM+y=qxnF&k$ae#CZCUbx*-R7bWe@-h<9r^ryA%buz~Aq}iM^8l@b&(~18S zK2RmGRn+@Bb?Tpu$RZ{a+0n7P2wy`K>&U(EJLsXTGHhg)-hs}`8nqC6S)s0`VFMX9 z@}Kk3S4P4wM!~a3V5b_+UeKi0h0F^FbqJ2W=o}t!VDNx($j1IU{O4MIS^f%T{j+`p zz72Fnoq4U_pmvV|t~A=wJ5-LqBk>JagXuP7on;)zTZymi`^3M!hR@%DO*0L-T*_$F zDfH+d2KF%HgicetMfyLG*Iq^b8~gLxg`T~f#Uycd(%u;8Fd3M{Pb+gFSeNYG2-bz& zV#L*8N(H8lk0Z0vuE2qmvyq-i8!OF;K4`a$xCqykb=w98rd4tdtjx`;)uHg234Nk} z>@J+Dc7KEU|17Ww{8rAT>L3Q_$Nc}iOdAIMnGM~&+%c<`v4lUMi-KFRK?;2EMes}v zQ(!J&qaKD+dFOM?S>g9$ZwSfY=(8>ZB|2Q=;eL)u37yIO1;6Ww{*9M`&1i}7t?tv^D2 zvev(??ahhOdb4J;mnaDs&d^p9GDsftss-M%mb4<;DX%xz8hjOdl<<(X)m_=;%tJZn zTIOgu`q1}GYurzo*1B7W9~9kJ>fpTcK>CwzZ=9Nu#Qa;iO7$FtrhWllSEiyzQHM?) z(&sxUC*$F_UnRC4iV!){KPK``@)h&0ideC!&gAGnelmQBns#lR>3Mx}-t+NGY|q=4 z$hZM;O-|u`%k%M*xAHBY-!G8%9B+|p9mo@pL^%4Nr;gRs@d){T#&@YB zk~-cTGePn%d4cz8>fgw>__6Zawq!MFYW4Awl-1`-@>icIS+M$pl3do^X6Sl#l%xLy zu&tgp{(q(3d8<219IHD@Y&>dL&$o1By_MY6DPve|vb3$?T;&XCVkKju|EbZ9%x3V$ z7S;i~$o23O*1Xy%=6WthhB04sgD*%O{(MqT zaP5)UP~i^)*t2c)P58r=Z3g*+tW$DUvK9GQ=6q1M5dApVp5M-`VWYbGnT%qG6xmSb zqpSn_X!8fGpLt~t&(|ySJe_wd?he&FKb+#&z4WLfr}BMAPK!1&>jLe1;jPK|VWt5` zMTD$_pFf*T)`R=xduqN%=1igHJy&MeVLUs&d3e z@NdljqR3>YeH?e^au;g``ETc0&m;3|5^~L-#6E$HR5Z^&K9lOpmzKakkI)c0>5tn2>?tX7Y*j($ImfDx6uXi8WGNkrQ=0>;JK_?*xo#Oo$;CYMr zDfp22jXh$z*Q}=tKbC&7?z^VI{5 zo=iQt=468&sK<3>9YPO<+&Sx~hf&Z2F?i6! z55eV9aQQ{!5~PhUlUYQ!`w$utSv?=#WrAmIL4Fb$PIy!(?GmRv%tN}J{W~qlc2W34 zKO*Mlwe!=E>7q7gGj4xlEWbXrq4XZ~YPkc~LK&&6lk{MH&BQQx*<06S9^sQaNY*@- zw{*71CXB~C?puorcPtygj*LvbyPUBtq+We`qg!+U%hX19p^WbvmRcw2HPfB$a>h6{ z)9J1k~-UHF(Or@1@$#eQq{N^`7F{Fn<@j2gXe zMf_anj^xobe3$#Rou}n{jIWNEo#GXyxeFg(Ra&zmhPo2wUN!%J`12$Emldrr&)q(9 zb!ic87p)jJx0w2CsK45iJi3Vb+ZOWE|pk_Y%kOXOnjLh)L^7WBu{3BVzX--YVZ^3~V1o&Uli# zv^5UTW#f1ua>fSIXIvQA8CrK}S;vQwGaez2f6f2=e3!ErgEC-HPS&q=cv1trQDkKY zGK1{Vx!OLOD`jNazoO8Yyy#29KP1*%Xb!t>&cG4uhxe(R4rnK6zfRGSy^Q~U-g*uH z4SNUcb9ZD@au@p2T+NbR2ybCs$YPH7bTY?@`Odi@@vF$UjqtciEk3I*z_T>;zmy-F zTvNfF_~`;PKTT*?%3PLjV!A7sNBC-mNevCvktL=*s3(ga`RNi9>w+16{ds*-^aPQY z#CP*I>Ut17h<`SZ_-Y4ZS(m7?#Gm!lP_@&{`vUTx#GlF@eBrGlv{BAa(95g^j@nB8 z%N((qZMC!cot7!TO|_!;$!{}x*ANd~7weZro@G5(r~oo+o~u3bR-G-Nk>tRu%xdN%1_v&2o671O9bwQU<6>s7vW(Sy z>Jk37o;epfj%)b8Kv|pvCbQ2=EXWu2#RjbC<7H-%HPvt*{F}ZWGx~Bp=^sO5Vv7+S zQPz)o_{x6Mh_;rHz6I#r(EI#x5{ZK}c%_c~$$bUv*MPmg0TfV3Bo&1e$>q{Tx+sdCp!|1vz@7D|+u+iz1{fxsJqaQ83 zU%s1SYR!B<8lh&$nkVD3(HxY)hqb0*r>r!oDSE=*d-VSNc8f(F<_xu*ZgXOKaerBR z4E+@RXH^!ceTs!fzfK2uYmtI-){W(-Nx@pg~sniV`sImuUeGtD^@wD z?^HQOv5rk^u|0OrRXMHLy}IwFeteZqeCowj_UuCFr8SQ8R(q`3)qIb_&*?qj=+%`X zM#Qm9k5|hqX^K5w%=w8b$07YI-cTw!mMPMK?B?&+Z}77)pJ(XUwHi$-=ZOfld%EOx zZYUKx#m_j!PCxDRGhO-#&-KyIh4eF)e(s>3FIFwgJ_Qe6NI&KGz+|=TR1N>>r}1qG zzv-v^uB4wWRh8MNz6ZQNR5_LOQ~t9rBBzCZAPeSr@AdaHx+*=Jx$E@MPYeC5;=4uq zxgf6>So^%Wj%AHQ)v~qe{(d%2QaK_=HTqV|zTIV3o9fja^naLI7PQGqe2F3RKwnqJ zPzX+@el6egCqq8N*9(trChn;spa+ZHs@~+-eIXG!g(uZkSNc`4k0$zg&OVv%#Agb< z809#_Rn`aZjm-62|6G5IIaLpjlsq=!2NJ8v?_gU&ID@e;0;?C^Z$~y?gN|%TaO@Tt zXbbc{(BO~H=`T?1L38ZhbHKVQ7v8rDI6Mce*|G-NX+Kx989w~<6%lib@V~e6&G4Hi zsj02)*6e)p;R8U$ckEUv`5se(qtC(moSWyUUB$dfUlB962)}%=Tr{+jaec(p?UYTI zI*wVh=TnE++0s`Gom+%|{<6Hc#29%8%aPwG_n|d=73H=hs>7l)UEo}?&^V${`!Bjl zpB;PF>_}VIY)e{J6mtJ;?7m*vgSS4ymHURJXB_;z%dA`(vz2QN@ox+5&iCJm-**0; zU;Ur+FRuUKeC*iGrHhf*mc-NzJ!gHk^yS|+-`|5RIsM^xHW&35w>xf$9c`Lvnw$Sn z-01v=66QM8kkM+UW0O4rx%%t#m(K6BE>+7amR7uBU7Bl(_et3_*1hueS`T)Cvf`Vr zkOyg}8ribwChB1BL@ldcdJ6c~R@s+!vz8&N_L$%^_*M@WBHwlMt&q8mVP}|5|NbZT zZqXV3ZXN%j{;tj3#JZw?-e$h>YBqCeQ}8bMXknc_$oeGx*6DYkZ%e(>fAZX-S=z#w z+d-RqDs&+7%gdh#4P0h-=e!5M-Tm~$OMdmjnX0D?Ie!&;hEBQkF@}D)sYuh>h@&}I z^Ok@APb2b)#LMKzsj`Wc-t2cwy;-+wdU2D*alVduBJzv)DD*tZXEEXpCUOVU`b>vs zofg~Lv`;&aZiRoy`7Qw^!BoGtFy1FM; z8TPe3*c>{M6=EpUB(U)N`pQ#=4L24&<1Wg_@@)^_=CfX1U%4o|30o6#W7$jBsKln% z%(cj$Wk(*-GEZaEoE4J)DD-3G-$#CE(8zz-T$@MPBX5wOx9shH(K32yQ@yDc*`iEz z^%wI@bDH+4+GCWtf&c3tG0)kPscKEqj;3W|cPmSm^f%0NCNehC2az$Z=eK-EU?_`0 zE|dQ3C+|f1(}e8wBJIT>pGmsZ73}A`>;VuSvwoCys8Qc;v$19e>r0n%f%@`+DOfHR z8A$So&JRyFY+qU<{tJG`k|y$?)F*kM)4o`TGHk{%v+#$o76xN2|&| zI1%RB06jzRF7|5X975*Rv^eL07F8oQ2suk>9Cs|{Hiq?7@@`@MK7$`+NRlJ_5c6O5 zbqX#02kGdifQsnT5XbW-o{$#1Q5)FS6xt#2={Ta+bzdwIvZ z*?+fh1<-A-!`zbwZGTbUeiquddH%kM-PAHu4VC^eZ(pv7o$0!R{w=KP&VG^pbz|qX zYcWRuHdB7jGm$;b(0md7lexB@{+$Zs`DbWF`d3aK>EGkzS21BF`pnTo-;({x&u|XQ z>MbRfy^cA#Yer;Re&?8D&x@TCRaY>lNDG^jnsduny0DqFk$2ut$UAMzVwE;8PPKe@ zQnb2mtX<29ivH^A((Rw^xxbw8>KeBuIqKh^EG<&a_gfif{dVinx6Mx*|HBfB9@KBH zD?O++-*1X9e9%kY8sNdk13wd4SZ{qH!-&5P{MTCZuB|IwcSV|(Tvz%SX&sbP6=v5x z{Qe@ldJB4J7j*WP&>K408s1$hqAgd8@F6>tb%pUeT=#7y{UG^_G$r?B=spYckNL@b z`MkYSWRR7_26wS15&r$Mo~Hx9+3*)goOC%b$#;B5LPwjr`L-6_TJH4AP{a`#e$8^@ zp1YMNh=qX8Gb*dIYr<7RMrC?-u4eY_#|ACBvJHJkd;=PGK(RYX{i2^-#AjyZ-Zrt9 z9!Gx=nMv%3;+GP>GL`(#;S-;#R8AtlDg=P=Ui@(u z>XkE2#fEYm{X^^}V(&OcJB?u~A?zlVDE$-O(9}G>W)bhHmE*D(P_NN%@TtWpPnbxgNZ*7H z7(C#Wajv3e-`f@a(ZlV?6Rqqo-R&9Qx_QklYL~sgqP>QAv3cOw1Ann@Q9X6&v6iK2 zyJXGM@8UcW@KqhH2&QTV9>bH{b)7!F6`see;{ECsreAe)_KAUdLX(LVXU(ycR zFUc+XT}gAh!W)pw;Tg(OV+Y=D?B2!NidVaU#hz(*O~c~ex3MX4zpIiylzg0i#{9Yimb1yLbr>Jw!%kSm`}*Su5#or4Z0P+VGCJ*iFGUu z!lA*}W#2Y@6dcF6I%(&-;Me%C*k8!p&O>jjA#I?*eE95*`l8cQEsJ~^QHwaYV^RO| z7~(9=E**I>R^p0OY+DBMUbV%sX%G8T<^hY$GqF3#J7(BkL$0y^X^ort^GXBoRdO~Q z^3Tp1%D<>Zv(I~Pj`*B>|JsyX9*(b=d9@*YZ_W|YYW{@nnK%oFq*+4J*x%6%zBa^$ zq(S#e(h@?_xHnhix(!JoX`}r6+1HNRn^PWWL+m!->HP|15X(qtszJSKJFf1O`1J(R z7sbXnbuFfC(ofZ<$)6RrpZ$s2bO!n4(2A(dYHx{iL%3s8T#|aNWu(Lz#Nt0N_rzqb zaa)1OjE=J&{r@G#@lcmz(~)cZ@u*Vo8T6RPXagC>^9QEd?GN!BH7B%wE#}DWck%m?du)sA@3k#H@fdf6@ZMtHJoAx= z2ba`GKDgw>W7>`v_`b#Xu6FDoeGBQG=DM2>k^Tn%&+@*&`(Mekk8dCG{+Ra|o}2i6 zGw*WVxAMM^_fp=YD3eT?8+oVkp3S?6cMb3F^8WC){2iGQ%a)uUviGLUv9Fe#=Nz|E z{+EvZMaeJBf1UX~et&Q5uS;?wmo3S${;DK%tgR&T(NiU*yz?JDT5><{f=7>)OtIQZ zW<7emg(4P##}$*1g#;O>C!2{+_J|hmt7A{>Vr__XQBtgPfB3!^3%+Js zd@avv+FHPq687Mde4hDZwwBZryC?8Vy8^$|GnsmNv;^l7E!Mch_%7yrW75N$s$z9# zn&`&nwAP~j+u9396}Ia8)TSdc@0cgpn)`g_^wt`_RgWraE#jNl%8oDxJA}^=2i*-% zld`i%&2F{xJ&o@c=7Zi}*sjZ)In>4c(N|cQ8->pP<+pPVM=?C42wpOK(b*dd7r|Tn zcBBR~F>0}_-P~6mTLs!2erbf$rm*17}Fa0C4%YyEhOWO6!@nq%2y zNX=vCd)GLVr(9{iD)_`C`S$4nqx|)hFTA4sLefl>JqE9vl%_qY_xkgC;Xk46oMzs3 zjMF^Ey3+X-S4&7cEx@pn_E_`Ti)Hp%SYwe-upK3@gnU_llY zpWMRJM~pSV$b&s&7XhiiLTki zTIY~9lhon)(BD?#Noeq>)yM&&8TItF{BX zguq#tkUZ7IThEM6_tK6P`{I1ymi9y+w_;nA|3agN4vo#QQoiA5(lwLw=(sQs`QFpH zU#;taa(TureD$|G&_2LW4KcVjQ z5?8DfS1d6z2azSiw(Py>Q{sz{KdyF&FU^bpMz4A>+lqf#d~{Bg&Qj%bzj6wgY|#PK2@+s%n>jl?$P-h})G?sJI&Q>4q<9fglT z{MQ}OaSiKfF7zdGyx4CTYl(lrzUAK!n30D)k#s2+L%Al(q30Oo&`phUl1J8mRnyGCr9LZ8MQsCz#ed2ymS zs%@Q?*tQ6{Z4YfVlV4;up<{tjVpcN9S7nlYB%^#&`8FRPoDOV)GmYPZ|Gy!Z%UMZY z6X(wn`zZ7*XC+z9qkIl{(F`NcNb-y_@}%J}3*uPtiye%!O{cq7kxy_9PtPhhkMwES zmvqwH(1qeHG;8DCzkD_{Q3!3^294Ybt=xidia5>&e;ntP@o`D-IoQ|a$Vr4oblFdg zyfZ%W%$!~uU8&}oOUKCYSJ*xd`i)5kkLQJaY-=cd5%Ncst^SQ41v@7Lo_&fz>(Yr+tPa|D? zL3L4_6IC3F{a)%ZO(( z`tow_AP!`Ysqm(&@g&{h$Qcf8RpL{c4lMZw{cG4GxTk~InnrNYXo}D31y^3iNN0>J z(yyPGTt`goza~~q%59DK(Pd*L^dS2M4~F1h(XTY>{{eB0GWM6@e%B>hDpU0vXPO#ts*cH?txMKg80e6x^|OByIn9aQPN{n1VF?58!fRN(kKz z;?kyahF>c&UsuQD7ugR#MD7r~n9S4QUh*dFX#3#x@K$5*q4X((Cl|a(nyf$R{1^MW z*x}`S1@eqlCHnSfsO37)ympN+-6Ut)9oAKB>pnSxkrmb zWnb&Q-qhq?9KOz-psjOTrmS)MrmlCtGyZY6p6PP0r;a_u-kOnd#BPlJ|9xq5GLM{Z zC~!5i21(mTuUBPftCwV-qwT}&Z?r*MN=3D8k+xN|uQPJJ`}BzQ?j7jB16q7d4ed=< zP41V&H@Ne(4esbVr~7on^ zty$Vm!G~5#JRj?Eqx26rR`J_wp4hs`6xG%o#P@ZQmd^Q%iRk(k_Rl8zwvu)l9C(T2 z*@F#e9JrHrr^sy7Z)Lsrj#%sNj$Dhp7UMht?~=9~`6e)lf1!gk8D~is7!6+wa8`m7 zN!Q66qeZoy2F|7coGpyiB{;8dU2KYO+Z%v$Rf@ow5r8wwfO9@+LMH;}5#UP$PI(KQ zl?I$KzzMxTGuVv`IHm0tz6ng?ClEMu8Jjf5r4l;(I%gT=j#EAJ@fQT~{Jy|_e#Iua zn^b5u1$po2No`0MW7Q4(L0d&Q->-Yb@#&jmQsdqvCi?fpJ)a}q`6RK=ZxSDUnBQ;m z`?^^lEY6(Ovv|U+lZ$Vdb#8I^*cVEUk9odi+}P(z_9ADohqh-QeNm*%8}kD3*jq|8 zhe8h95*LO1B{&itp$d4t#03b>WAF(rOnJw>R*Pyy#xi10GlFM0&nTXIyH@U4ZcQn9Xq&#|C)?te{C1md$#1sVCO^9^e)6|w zeYUvW`iqjElD2%CKKYxo`WOGq`m>T1rh^X6^xx4u?#k9?c; z$>O`LFP9Y66F**0+#r5#4fs>(>vN3x$+6o?R*!wYCl`Lg~~c z-$WM>dL%}u^{H=#d!Cr0`UK8(^OdLZJDO+h!zRyD*O@)eugbaUSNApTm$Pi@hNJr@ zw)Txf_m}@2{{J5Z|2swm^!dcrbHTK|Nu-G%C$aTc*9OYjhLbj`_3g|4i>#g4`U+`w zWUe)FYG+UmmwA(h>|ofd-%@`6=eH-h?P0j@;}cs=Lm5C1?08!|2^UWtW#7iYg5@FeDz1fPfuC)(PXCc7C67C zA1TyPjPL(-`f-T30c>uyjp%d+3{sxoUs@iRr2H4vuPzLf#eg5X7Tq?Hv*<0Xg*B`@ zI_E@LCd$6sfWICYQhb)7@liUlP2Py!EDLer#0u3eAjY+u7@$^SfVxJr)}>(M!nZTg zx2p8ij9=X!8Y|Spx^NEvMf`>cSK7lbZr1x3wW}|S6}l%7EA(J6Rw$}Q>X29=>w3+w z#dJTnv9$W8*88JYv=V#t?e-dCg$h%mM@zhn;Rh$Csq*WUOMSprNxYC3pNQzusbWLG zP9^c+-F%aHA<-dNyNYMiH!pAo;-P96kk>~aw+H$-G0?~B>Emho7)LzR65^r0OWXc9 zs2#wvh&ZUYKpfPNKpa&3WpPle0&!4vMjTW$ee=gb`OH!2#Qw-0sj}*u{IO6)#Ksp% zY&>lgUr(HTAQmcmS^D+J5WX~QTgRzK%Im=KW#u_vrBq~mGkjr9&lT}$A@O%n z(ASmtFMyZ-J=(fF4(-JN{{KsHX#Z>YpU=2miGQ&%h2pM|wc(?G5O=bdI*7ZkT#35_ z61&LRVCdj=k(@{M>8y74YxImGcBKoN^+2<7hEUWk&_6!X8e;j{h~;Y|mTx<;d@<1O zB4~Fpbaf8>?kqHM7Me(SxNWoTv*PvyN6hHdnd9yRAsZjIgfUn7HoGbFy_(EexB~DJ}s}VcOH(3kBo+|LehkGiSs~-=}Rbnvxc?{frSq$c# zSK_YV(m4#QqedLAoj6>J#Nk56jPFJ}<12K047o^RJ&ObM`;Xy~wUHQH9XkG^byes% zH4uYqyDSEGa9uTGaHFq6$CW8DqxAqCr!A{AVsIOwV=r{fn%X0=eL}~wrZP7TI+hst z|4ICIG<`|Z;iD3WpdU3t7{*yg~}eOfjDeK_TZb$ zCGoq6-1vp#h`jX$=Z@3{&mEC*`MY&oofZb`kh4otk-haR;;_Gn7KYMh=$hRKJxHwQ zpuFSnpFs~-lb3>P_8nhB4{KAPhXbEK2Ul#b5_2c?(FlDsLLX~Pj=nVbgsku85V>1y z_R;W(#=paFWvxQ~-YD|79X{b-?_x)5#OkLItDi!wzAX@|FYCK@&%CAcL`JkQJ*Z>YB-US(AT_E6#{ z*_d)XY|u|q5%7V?o~%mj*|KoA}dv*K+<7&j9e<=^RG^XQ#J19emKK2jEci=Tt zzT1xtU{Jo}o5*)!2N{&b{ApKNS1zZ68=(WgZ{q4_B!{ju57<6`4qvVs){~0;M0m^e zPj6|Lm{OfsQi*Mqc=Il^-X<}nGM1e2>6E!?{fX*fKRodr)w`mJeKo!rb5^}F4ja`E zhB`LM863~i$1U73BRuOc_dHANuTeksJQYUFEoE}4tC6}aORILBGA9;ijDIItY{lEL z=}SBC2{0u8K-It0Ev5t6)SY3DS0IoRZ&k^9wM`oUs^5^8U*sHtVEtiwxIUBUcY*qc9(bZm`>gK3g?Oh|drWh89`X~OS==bej9 znK+dl#DVjecr+fh!-)5lJucWUC3e{azmGw87Q5PPXt(&n!|kV`n={~h2)N;%Dqjb> z&pG@Ihi`H0lCxcEImO2USbGOFzzzevNz^uvmCp&EvwwMKt(#=jCp1Afln*i zf40mY+q{}Hd1W867ys1(;)QgR$tmTwGIr0A$4g(hx6oM*ev7ypwiEtl#8>lOd`SXh zH0{SgOTg!vtec#<3iueabBtFeZOc7A0-uSz$AGyD_}ItVnoBz-`uqra-dt9-D_=!8 zU(jFKycPJ~SZ3daoG$0S8t_4{t<0^R7VP~}PH193c|@+t^Dq5ggO4m!iP{hVop)SQOY{AEZe3N6vhG=0N*_V|l`>dVNe<6G4X0tY|VJ~J~ znCgqtG@r;ZjXM718`Lt^mUBh(Mi_pkwG&OAC}OJCRH$WX>`7R_{)Dy7W=|FMj1`_k zJ!`3lI-fA~jc15EYmxgM^|mJJS`)AO)>5`^wb@fm9F>Q8Eio)rybI7RW2s{vb;uq? z?pErPGI>%b<;Jlux}LIL@wWqyo%F`%&7P)>YFR^?>Z|9!#AF5Si^e?qajbtHb&PhU z5O4fAxHQto#QD=5W3XS-chbN7&d{-kAAho;Zztst%cE+s zC2p+Q66czj)a14&INkVdYrhuW(709dU!Hjvnv8<3ppyKXK2~b!w;g;fnUQRcg+spH;Ls znc;`p)A)3sN{;!>ciR``sX1}|-)%pl!9$5%*seK~WBy}l`=S&{UwV1^>4(}EB}n>1 zm!~ry7tznS{)dh9b=1>BJx=;vqp8x<(RD+kwdi7ej>Y;-DlwKbU*mqT7N2nQ{d!L9 z=tWbWDcw=^^ZWG`*Kdwxe}qoCSo`|umKBaC_S~Y?rY=X1Mn0SVPs)W+J+T|VY7u$X zrM&HaTlQQL<5-lSI}3@cxg)``Yy)+iHOHjK^Tw{5KIGw_Z_a%9rOo;Zu?bs~6)nex)+b@g{T*VPv~y^(c8bzqIvVvEzjb!5uBZZENDk7{~1@nYN; zt;+69ax7ar{#|!5I>t)u)kon`<>ML8r<;;Xn1_cRX-bY?QCI5XE1|CMH9zBnf}1{#gkJKoRL zVtlVdr{WuWFv;n5K3X@lCfu3)11&0gu>NkTzwUjhU+O(5bMA#WfBYx)cLnOt;k&eP zCv8L*HkSI>M^&;Sd~W<7HPWf~}JMFlRVCO%>nm3=dY_IJ5+8g0YzJ~onCrz)mJoC)?#uW!QN3aLMYmQ6T zQ`+1o;U66f;$5AzCowFtcAh|vISKFo{(1JN56SQAkn@ux)N8XD+cn^W7^o4mDSNd# zuG1noQ$C4x*Ix(w>IUm53{$U(UfcnkYs1x?B5<_ORJUoJW@!_?c^VidjepzCet_3D zaHgijW!HTZUy>f-JOr)0sp)I&7WPT<`_T9{_o00%;U&)N1Q+b3Qoa=WjNRa}{s zpWmoolVi|u@Vn6TPR`#S7-Hfs_~h;T?|?91 zo7v6zd_@U+|IJokKeL+n$l{hYrO{hG_uJ}M&5Vg(`~JdJE$0`S_MX2xGh?%@zF}rM z@Dzuyb=&G!&$QJ)!t*H4V?2#BeKoXupQ*2FTY}5h^Y~1a*U0l2&!aq#@T}%(;912} z&qMorOnu#d4cq-yZE02qaP7mso!LL|^vCdpl5>T_@v#0VEiu|;Oah7w>?W_%VR#ojfQys`O>nx!t#`X~8n9pwB9F58^M3wod^sa{giVGws@5AKkjh^0s^Xj;PJ+18wKg zb~o*H)#UeyJR)*kIc=N81_8Z^%z5RvRkN_6`r9Zy#h9F?jWgIDe$GCvjmXGnwfKy4 zSxw2TeM8S`(HTB|N1LM4f6o3t=A9v9`p~V*!Ry}J(3>eQKKO;p(s!ZHxw^r5PzDxQ zOnK;e;L!(NNPiY#PY1T08+adLAJ`5aiO)LC*-1NGP4|c{#omHxt@yjkksVgi7w+sx zkh!=6-YDl0;e#I5O?`Wa$2o0ox+j?T0Qj?0xAZ%MHl=@ez=t=`x3}#(Hm{`*MXaI0 zJ&bQ7CyR_+2aG=QRtIG1F6N80_af)}zYXs#f(N&mh85#qe4-pa{KL#2xi3y&59;S= z*nAZl4!a5sYgeIR(^Y6#?eHd=`~Jp!^v}2bnC0_rKNj)%wjYc9 zeB0ci?yWKReR_cXhdjdXGQX(p|A*%8C(ug=&pg^5O4~AzWo;CiJ_X&Mt|{m{lUUGq zjT1#3U5}t|iENS1UP7VQ zRT;zykna|Av@-)8J%V_cH}{tBdVyz;il{jOoxZTK=^kk4@CoR2JA3t4LZ|bgOp|0_I3ll=CF; z3fxCjWX%V_{3bBV-1-2R-v;J#4y zF^v!U?F!Q70Oxwo~F=WIqY( z6=$NCXXlH2c*1{nfaJ^IY%KQ3BxGQdl(SDw=tnl%*4X2p^c8e*_z-6S3^Qan;6BAT zNZWZrGuXOijgt5$X}<*>_^2t&M~p5qpPXABX4GR2)FWxK);NLtJN*MAzCs&6gkC;b zpmI1nqbHB`FPSv4EqIX$<-Gn?)G2VL$=O!id1>q+;=km>-sTcGecV4Ua2^b-o$t`T zz_W_Cl>JIoR`x~gi60~HNV<-#GYHGt7Wk&{UcLqI!8t}uo0~I~yx60p-oDZ1zFsT( z+Bjt32|VzRhr0)!9%vZL9+O+x7c+}}F$L_4;cV^u^4J&irm;`nkcn)O*e{Y=M*#e@@vV0_}}-&7-}E{<=3B_D^hIV*ffh z%IL?&{JJP$kbBCKT3-paH_Nbz8u{}mKPFh-5j~jqXTh`%DKF>tCbhmFtkdX2w1Jyv zgZXTi+Ic#dw%20xU&Zuh9}n07g6+Tyr=8BlCU}MI3fcRdJz%>6 z-uKpFySm=6U9B^0S2rfCcYhuGl@-1%eG=Q%JZyvkSkH-GC$I`^TLQ4w0;BYYx{>SY z3;pt3(I3fwANkszfcNBbKDVZCPkj;|2amM$uV`QMqX5EfmB8qtNlJW1jU%E&2 z9f99JA&P8{(53N*-L$hZ}yZ!?nA^T9Ku$12wT}f zY-NAIR`xsWSgGLa404p2_c`okiywMvbHc-0H(OSCHqRuj%bbvY!W@@=VuIOY*Ty(C z>}D@0jX2M6=L~e&e@oCoG?@-60hjwg^!|vvfp{!^x%A4dY%Ddiq zr=~d%lfPBU@I1nE#vEg`JLKUPLhN*tvC~aruY~42MV?dKeX{U|7d9V~{6kdV<64|^ z1ivf4wso_vB|G){+0ICK_Y=d_&~w-R(QSEnR(l$Bqow@OeY5N(Rb$uYVypYx$q3IU ze+~0|I*z@`LZh5RdFtt=*zKOgZfD&(q;|uU4asHH@#KsrldYSyT6^Y( zd+o&TH5|LwaO_@I z)9{)ZnNKDEh3`38&m<>f_qqqWm(`S9_I)bckL+(vq@k`<%7qp8{2)}!+$WE!q;Ml}ADo42MpGL~MU`bC& zX?J(2A#Ib%cLQ@|rCIYGpU^~~5_}a6d7tDXXY?(Ia(T4`-)ZKycL- z9cs$syrBCPd#B^rvlG**b1sV90ad7tUHb;I(o4PAgY_`yYSN0vx4Q-Q>#I_;Z$~cK zh_Au|Tmr*eoEeY~-B?YU?*m|vb_vug=$ zalqE2;@UFVqm&t?Tw9f?ZT>^1%?HRcpFQmPDzPmemYc7_+K$l|!PTv_cSObeX0dlX zHD%T($Djx4&m#IW9=K&(Wxh(DRKD3c@9z-*^Whi%eucLl#jYd%(lGiZbQK);F4o51 zvG3E)y@ht{t_328Y8Ib~cW#vGIRMRhWuJE}K40z(DCB=V^zj$$G!fJ#GDso$u|1W2 zhdqH4X_q}Z22BZ^vM+2a?-qQVQtw-|X914jf1$k=(!61Hn=0e=%_2|RzsCM1@N0$N zd(Dl>zH!6svaibUuQsfmDgI&3nfv-1lY~Xo(W?jLfJdg1-&a9hIcT*$z*OR}F{B`87BYz$F>&Rb6{;mU& zE*thr8+J<@_DkFNx|?j2vr*ngJvQpI6=3%)!2Vg#z_W^{o~LeR+FbVIo7L_MV#|c@ z)MLy1>>~CA_>lbmoBXDJ`Q6L!2Ur*8V&9x+*f*nGhJ0-v7O-zdxy}aiBo3_n<%gt8 zo%1L={}Y;8aff5V-kY|r8itScR{kgOzK!=# z-na0M-*JwzdMWolGL8=PqSkw~KI`-7`nPk21Nwo5G4PS+6!H>2?^dyKEkS2Lqz%b9 z8?ZAR()@OYF3uz0fG!5D`+daN@2t)6q!M%i%4bkl#xmP3bSGCc?bv}em-C9SL%XHT z)Y-%!KzBx4%%crjH2R&|`E%Z9(Z$4$hp)Zn4Eoq1^f9rGoIxM6HcYCKGe(|72e|+* zcR;T`+B>2}`_7;<$@huqg<=mB9rAY3A;G2ipu`^OyZ)ukp*rNefDS3PM}_V26nUKi zyJHb!TnF3&@74nJHR=?d*MOUG{(E*n!AJ8g*a68~9l%AM&`q!&U^@lA|870NX3H0Q zVkGtWV+W+~da3HO;cst_Fz>M8Z*PtW2!HuUBmLzc4d2nys`@S-7=qnyh+(%Y2rVBG=Pw^I#9uxl#$P@n z)?Yp%dPj>%>-(@_D7Lquu6eXSmGT}_Uf*fP{$Ai%$zH6=2F`DyuPs@ePY0e`TOEDd zq1RL7IRg&QGR`m0arDh$ERQixN5O~qM{c=GU0!~=oTI0;D)5V}?X;oi|Fcrg)!2ek zcyd)(O(%4ff^9`%Tj_+ZY}i(~58kj}6*4|HVC)3e%$&Up|klnsIoBT#R*fG^IG8kjtaY~t&rFe!&dk_aI@#xfZGAw!bguYMkj##1Lm;U z3!ee*K64KC!ciH0z^WVeLOsLF?=TiKJW zv**~bt)v4pxPIfm0do-7$DvC<%=%Sf7F?eU;2K%$GMF6`{J7>G1;dWDhcn~Fz8MO$ z*chyw^EA4CqkH8x@J#$b#l?r)SL5>&`uH~VA@*3IktP*ha~%3u9iWfn&_`VeecT(w zGj!>Pd30+tcz!HEA1Xi}f@h`tc=jqiy*b1#+mfPY+$(qv*xs5{jPE!w2ip-m{|!7h z-3kv$!6t_7?KCn+3wKtpHs(th^JTRk|6^JA7Q!Sy5AzkWnQdwtBw&zPrz@BQ2*di`yV zU3aM1jHA#R<2Q7N=AS#asmSzqZ&MOaW~?8-53V0$=&Q_&0Kb$uF6)QYqw=PCaIeSSC4{=q=|5-TQa zRm=Yw*tti_6HjxRd@aky3(s-wK^~UA{O#neo*md0rGM*@6D0=mWBMcVL<)HQ(?(5`(BYa4Q(!pA4PXB9E<7TWjnP0kd1Tl^o$-N=tSi}4r6V3#H?qs9k*YrwDA z(KE11SE65uT|<0?68j`|OT9ku9QR6i^m#cpcAQWs{}O+O(oFm6=XLydG5@qgKS^Z2UDbMJet zy#uVBgvpGck^~$Qa6-mZC_6#h07?~5TRRY>tqEwAqgBx&8LS3Em8~c(v?YM8*;{jp z8mja>=OIArL2zJjXl+kTpr_p-4$SNzxZm$@4P++-hd$>#pU?Y#-aqzdt-aR0?%}$x z>Avpkx^CMZdf0m&8|*x9P71B2?hl>k>x``d=Xvgc&}zmM<+=TRg8g-^_sr;++h6j` z^Zir)X~PJfRL=dO{em07uX&rm6(9b9dGKK;H_oQ|Nk`*ZiT}4Bk7u2@{ok=ID<0R0 z?`xfywsS(u{Kw?yrn87S`=*`qf!aB6asR?cZZ9*w znTnn_HB|IW=J!p0Dg3_9&*JwDeu0Kc-wh4bi?45}^7U+}s=A@!$7kKxP;pi-elz$b zHB=0`zTqM6mqwb#R}AXWAYNz?Gv7Rf^~~U&Av54>=rN#Te9!*m`RYv`p?@oC=Zpx> zmY|Ol&GwqX7-O!_7(0Fnv+NB#O+&bd%S_p zV*;b*G;^79tQnlYVDimh_@B#Jy2_he;q!&=eR9tJX>AhBM|Ai%%xE%>tk1(C$fhYUq5{)7>j`; zb%UMrvwddev{gL67yY2we`R}4l4<+OHl2t!rbq9`|A}*D$-`juKqv1tzM+|1P@0c= z|4p$b>QGJ}<;YlupZ6x({UiRqWyEM`Ja~*K4%^uNdpcO74y>*`sTdj`^mKj4hK>f9Bjo# z*uq?kZ>X;(_>JvjX~De9L#-1BS$kFb@VCG|Rp-txa~pb_`4wKNzn{=gogZ=dYhPt=>no?u zZ+7Y&Wc&6;u`^rHR6L%f9GC5zG!J!yop~r76lz|}JkXarfkz@9obSTNgUw#r5^R3O z)qD0=_gNj+#CSR3OLym&Y^7~42KE0xzeF}L=e%w^ZqEFSxSP)rGb7ucg)US8&-Bjx z-poGG|J3;<6)l}(C>{K9&M*0Q(a#p0JCp=|!bk4s-0*6Lm!u&dJ(?&+@3Lva_oL^J zRDTLhkoWEsO{ATfCJMpD#Pc7$v-uKn`v>$*wC~~ZMJ^tH$a(GFvkTp4tvvhGy~|#~ zo$+30$HWKHgPi3J?&nrCjcZP;7@yDCzR_XWS}f?_G!M)sX5gNH(Wrc5v59%1Q?wrl zHv9=bum}3rI-)s^Jb)<`<6DQJYm@W!KH{kYeycVwz5Kr$j^6aQ zg*ru}KBID4fd31VliD|4;eYF!TdgXOP8Bb|Xqfuw>w(z~a{e)+K?wQp79_9DI zcVtf&;3ff zbf+3$eym>k8W?Nqkn=+k-c5#A2a&<#)U6F7e-q~;KHX5>R%G#yo^PSN=HY2zYumSb zRgJHg4j$6nR*jEOOtRa``3bty(^fmU9WruzIQmm=<+SHCPwY=0db*^21U|qm)GMCv zxcS+Bp?keEu z=~>ORi4F$ePm5ego0px{Jxw#_h#?$DXBiAVmm=e=BGyUJvvij*vDF2@Ezgy48Ll3q zT$Z9~wj=pj~eTvQeU&ucbK7$T^zdw9(3v+4HWll`_ zZ#hqXy@N{zxHOS+#gs3?7v%T0{v+D02RHMf3(fU!p|{qFxted{AJSZ{%d_Rs;3~fH=4Pt%Mb0$l%CpRLk8XPQHzih#f1YwZDA)Pi5AenCg{tt&Uf9!kt`wXt8|>tI zv&`~Xj_f+Lxv|jymT=EI*J3mM^X}DtVx0l{osSHKj@zU3FOB=3;iCyI5<}Q=E=C_5Gqx`ThmIkNf@PWM=>SmR{hJ1x^LPtz>X81sr6*^}CbjNBx5%+1MQJ z{ciI7{L-}0+4FCXC3D8#3pKcm8f)|LpDZ3997zTi;KfzhbHxR-9c0_I~l=ih<(Q=-Xd{H$$5h><6s;zQLHL zFt*8zaS~%KVa#8}hYFq3@8pI@zV0Zlko?Jx%z`IB0erMhYSNm1Yqr+tJBoAjY`Z|b zUx)mi;5QCjd3Nx4Df6wlolT+qv&jGe?vDVp~%$^qX zB%hL|`g~nRJ_JP;}XZ6jlok?!!a_C{S!P>BAG5pcjP&W{nadyS{Iy?Wb(XiTA)PU@6 zP;U0pz5{E=k)OMYIqyq`dPY56qd?${;`0QDfu7Ionm2!MFLnjQ#=2DBdzK zGq5sOHUQ3n$dwHMT*yV#GSsSAA0 z@u%DH`48slp8H3}&h(v4CsTpX*Q}MQBN09?@vYtqHZ_#7?IXa(wvPZC!-b6rY%)`f z&^>$7Lp9j{a?H%O&ER%RpMkXj`Z1Vu-JAPZwNFvsk^8!z3c+NaD)o-ch!`o_#(`3~gS>+8>k^_lQc^dq_%1^&GV{_Q}%3m-i? z`eOnODW9wSEMF#v^nA+Zf}<1QK{vCGjry|5WoopMV|Iqd>f66ycYUAiT9cgA;xFZl zPdcLLZ{L08F^y#sW0^qC)S>4uT2Tc|)FVrxeX)6PZAM4QV_f1> zIrcR@(sIe^G5px|?3}`aYZ*Bg*~@911&t%q8cTt@;?2aL4*n^=Ts{+rdA=3?DE&Zu zMf~e6-boLT|0VuW2kLzlE6R2e1Dv&LV7V>RI_F>aY?&tY4@P%Yyk8QwTiL{-y^5aC z!Ea|4F!$nm|I)`?Ho@>cGn;1y^CZ58u8rLX{po&eKYo*a7@fy0SzWQK9R4~Kf1oM& zo)9-kY!5Myw3A9Z%9~t^ALK&pTz9b-Ifs2m+2h;%{_T^^l*LmPyty*&PiZsJ4T~3~ zuS_=f?;^MV_L)YqeI~~%jDHSzQm`Ot(gfYPvot-sUeTpKX}l4*u@QKbvt`DwI3@lfbujbF97;t@8nwR z7sc)PnC)zn?U6RNV&jfY^o7(;GVP?$&aJeQPCH9z=Nj7C2!Hfqt(MRZz8Rs7Zaa#j znv-PM?Hnc!*b~fA`3^xLcGVoKd5{`>LE16^Pc3K&DC7Bi-99TJEmJv=5 zAXlrfUlXHPSM__Y4|2ucO^jl=>MOCpD80wlO`Kz0)n=|$>a%6NL5>5-dL!GB^+u8->rKOKDD7jkvwmD^{HkeuX>tYWP%EZY(e?(~h{iL+ zt1{usjJ<~E^3AV6o{Smd3l#!?ybTVR?0eSpF1nHNsb6b*gzD)Jb3uM?{I{pPsU)o+b6 zdL=c~RlhyX7*;X9zWT;kbgka);@xiE`5R`Y5VJ>YMR>@);osgeKWyNK(#)Ay#xV4q zoQm=L)*?G58tn#tDzb^@mKV3zr5KGLZ}Erz7h|ZUe~TI8zDuG1hl?9ZtE za}F@7{_(ixhl_YtId06X$@5#`<;O4V8$MhOZ z7N14Q+vXwJ;dmxz#7Zs;HrrjeF?Mn%!asuT%GVj>eSDxXFHbburQ9IODfUACT3z2| z>0Ac?*JFDb%WA$;%<9%I-}j-+u|e!Z1Al|{jLJIk4?4RsGx7{@uRtHE^1nRJtTh`d zQ_u_XK}(#A8vj>g(L0THUp0O_E}R8-gMD5D`1oss4STA64K0HAoesR4@days)PZ;K zNPPW0&@!0u8{R}U%Q1)&^(8joST#9m##6xI@`o#Ph9h1R8)^ME1dZ- zDxCQ+Dk{h2Gfv@s26(S|8AyKHbjz8Sz)1XFDyqh1*7@3-sCO&%_V!?tfV=Ii&6WY1 zZ$JZG;qpD;B3`F?EXy?7M^UCuF&t$Bv<_KU92^{4%^vN4_BNNP?mC_srMaOm!=ELO z?Xs4=W?5IEbh?^%d3E}hyv~fzlYK4pRWdG~+VPAt z1kWlke6=&rkmRBMRwz@;T&Q;H;sm7o@Bl zie56=Ox>pcDV#l|T*hN~Q__LE4a?=#V9QW$6A7@JgI!WiMfyX-xE{Bk81JpDj+@7PQ~ zYoX4zZ|RgaZZ(}7T8(ZtjQKYD7>&Y>x0s9dd-6grf+N!Tntc=Awn{lS$Rh6&^3bG@ z-t$cK0@-ZdL*8kO-t(D*WUF((1#i;kkB)7XZx6IpM!hyYpgT%F{)V$yE0CwM1E2ba z_T-CEitJLG;%(l$JodNpu1yL3;l++NY&tI0*)dL=*>1mb{GIz%*ri{Y^T)=-H`Ff+ z7(Ut4XuklMHxDsF?fuYKQ=kFj)}sERaO<$*@Hu+twxRxdeOW_a)Q7D%cl03@SW@ zqIsebdKKDlO(XX=>y-)2A2NJ~ZHuq|#kf{%@!{%0v3mcxF&hW3&C?#>y4g+Vg`VR2 zU|oJ_%@$*49s>?`p&_S<~>yfT}gm<6Fp^dWko8LsCWo425=x5j;8 zo2OJ1gw&sJP>;q~N10dpX>TCC|@vE=xLidl#}s-(|9YlF_4L*A3w5J>YMQ zb*1re@Ptv5dx>%f4cuftHh{SkpDgKR>>OwgTuf~K&bOK0nZ1f)wF28i&LCFT~F{Sn}kWJXeAIMty<|p%amk$kwrKE@aec`(2^h*Sd=s!(rwn>hHTfnYMeM zpS^sQ90Fb43Pc zS#zFrQ?kvQAG1=EW0qxXXvPL!M%}g_6FPl-4E%Va_C1Y|+D2wCD`Rau6kIgQ=bSL= zbNi$)SHsY|WScWHUC#FoP#mkIt-_ zOc~|9nL-XComqHmp0RTgWz)-tjSavnMsj{y09cow3=lULsGUT7Dd&y1Pl9ihvu@0& zJU3?R!aXK5ls!_!bfEi6FGa4Ly#E^L{%Yud3VdKPJYy1P^*Q#^rmCiKn^=d+*T?49 zPYvnJuX8UAt*JJ5nzX$#*<2=_B@Pd^^})KK_}e)!eh(N+M-hCX@8yzDQDSShqMNGU z(9Nzufz$UKc$xuz%lpU;8UEE(%}6H}s5r?N!dh{=9tcZU6s$zV_ii!PgGK*Y1L^t%p9Z z|8L-HRiA^ebuIIG`C0{ht$#%aU+Zsl@U{LG9el07@#%c+DEy_V8@_hLO zUDuhftxPhOt?$a${0?8!I?ds0?6YH|I*YQRSIJ##$;?<8d`-OU)9AJcrs_|-p@X6wJ&svXRWn$v8A3argIy*>Qv3h{RYl5PUJoHhA~R^A5U+q zH#_vUugh-e>TThZy~fBF@7Juu;lCTo&V#?9+x-LhH4kxguxEVW0kF+K-)L0a@)~rt zVsy1V=xSByYR^32TX`FLjH8>SEUrVBx!BdsSU)T!1SVwbHP});UCPsIq^EV(QIZ$u zp`)xWpB0-x4j1WZo<5VPr%4C7HqRI%nfej3R{GuG^3FPwr>DK|Cr%cbv*i3fD<6T6 zP0cYHO~&!z#$b5I0^iF0AMDs>FoymAQ4}^lY}#fJKbyGA{c~JJa+32CO(Is!8j~AX}jLFM2LNBv_ z@p3oi#oMGy9wxT^VPCI(x#(brjiz^(p|ic2I-_=(&%b!tCp}g!!{zu%bhh`fTT5qq z5}hs1y>zxG(b__cdm6ZFl9pF%kQ_ zlNe-sOcNQ??0!w}uI)Xsb~bH%_e9FdB;uTZ48MNQ>|u}T$MEYo_ZriW;nz979`=|X zgkMh}cZ|j~$MV-sJ`jAojXaIdkf)K{@++snGs<0lk&6x`J=#E@mp*3eX58x>X6alF z=v586qE|hG{I3=OzblJ)FIrIzDpO7W=8EA9Hj^?`v!O z`H~m+2WOuvpGQ9(oZZjbqOY%Sr?th`fZcC6r&>6x^EeaN7GB*R&bImcZBJ!B=ANIr zGBvqh?GncP0b_1p%u5#ZUilNo90BKtG6%tgF>9Q;<+q;D`1^3LvA#}UU)Pl}zt;Pj z+Kr649K7qzn6-wz4ZV?=*wAh0jUVK2_GI}*W3NHKgqQ9VFD+v|n$OyJj=Nr!{syk> znsjmC)Vj49I+pI1=(nJ=eC4}v@Io`a)|?q5CMHyC=ET})BZZto!)T+jGAGvEH(IND z{9CqpbkT14_a1a|>7ub-&K|2hZ}1!OZ2KNs{k!GZN4z;bA>PJ!n^WVdlSw{)8c#05ULl@rvG(~QuviZ)j?j+Bzpub& zMf|(|TqhRzH}LPT!M{I(e?JQUe)fUBD<2X6z9bmFjkC(N7QG~qe>Yzg5-ut~_h5K~ z*4gvGt>LUo#lQ1dW9C8IW}l+kJR@cC;1j7UH<#ZWo9XiJJoKL7M*mKQTZ-B!` zpdV<6@5=A!RJTV9*g|bum<%mk4G&v_y>t?}76Q;}+l#?)*@C_+>p$4K&EopumZI=h z_ul$&>o${T!{Om2;~xm;Vly~J+rf)N{TWl9(HDL<%Wk(bzjN#X;&+47$!7q~=8+Gg zyYipL?}ns`-}Qyx^*_b$7AEk!jS2ite6R%`Cw`aX^1E{QT|olBD}diYH#_H=eHW9D zxwfSMK~ge2z1sB_kT( zPYvwFcdff)ZXG_Sxjhq~dxr0abu-q_!{-W+#~Q16+HDCutpuKijdi`~@fvuO_}?|~ zCh@=CoJ*s!SHqjqc;6j=>{?eoI@RP);g2ow+cWXGGx5!*E-)P)2K|Yqb6G=+Z!Jr4 z_|_brjp8?wU!qT;Y?u$8kL{PM>>axIY{U0+|2X?V6$69qb?gPz43b^WC@j(a1=RFiJgUD8TaNmfOFv5)%S~>-&*3^WyJAB= z?Xi#=*&E!x=)B_auyca#bMq=TOvMM_J+pUP5S)0l{I*yF`1xo&zFsn4d2Un8KDDXH z<;(C7n28;`i2d1=%hud096OZ;@5*;Pksj`Eo?Y+-R5WC`o_YBcV=yVcKAarQ-^;!d+KU_ z!yhnL=J~$=%QgJ+&`F0IxBPPWpzNvBD03a}H@Rhsc)!^#b2()$r_7y{naBGUx6CZw z?{UkFpv(x$ETc?__vQqrtxDee{7zdVDKnBXqZT*+;0Av8@cSvh!a<{^-ZW_R)cXgG znfmiVW2X*&c+b=;AO73a?>+oqQy+Tx?^A~l`r*`RgYKSs=b(G0E*o_3)ZB-fr%rrm z@6=l!`p49z5AC1Yk8`?b;g4VXVDr>weg^N2mkxYKI!ncad#2X$tVz##)|Y1+@$oU3 zw`@ee&GmOO3wdsWA>5SQ}b#iLXy>Kk^4^?eIQ4 zr0X0T2jcd*6zs3s&b+RL5mEgx(czQ2TWsac1xQ+hW6wev!+t>=$`$+btAbb0slD(@~Q>@ndg0;p*?!C3fdkJfexGQ^G@gtLbNG)$2o0?#s z4KR)Zbm{_h>H>7?)bjGN7bBCCksFd#lFM_DAIZRD=*eWvq{-+FL42%#Ij$BvZR*IP zaIKlL{aNq|e`C(q2y8E^{KYtx&(BL<>9cj>?)Pun*^eG|x{2H4kU%%+x#iq_(HV!<9 zuU94bsJ*Gl*DroI(nEP^_V1d&o;bSP40z*nEs81Z!9Fr`e;F|H?A5&~w-6p%3N4VE zv@Msb>b&jx+ILrRedPKj?|Sy{X9~8jSOOfE0LNDgUd4|uwQUyj`95|6`4XXvZvR(z z?b1;2`U>*xZ1?tbUN3mVF27{cBP*WSBp<-kwo2x8G3QA{j>p$m$lpFVG;}$AETE4C zJfC^eI1n)xtqAnqwjzIZ^2!|g(~Mu^sr~b6#RU^nJ(2i%;EZoUd!^zQE3&knSUUTT z)X@1{f3>bh$X9BF{)T--ei2W<-Q~>MiJi~zE8&c`66Xv*`%KLG|F6a)f?xlqXLt!m zKedj3G9Gcw|6n}gw{~}qN2~((yfYnt2L8z}BQX|Hv|CRZZ~wi(-GBG>XaA!IX8}RW z-oC%~`-7YXq_y0^p6s*Zqac4D?X!#bHxnP-(39wB($AV)UlNOTZVvr*d`X<=8ORgO^~O`aB+m0! zGaTOq`&)Qb+Wb+mde@gkJkIk!IC|q%9X<$dTRQ*DG>jec7b*OW{szz9Tf&&5=;9^72i>wU2#mB3o_wCUOtxRmgZ3?9yp4kK z)6}Q^1Lb@Wj+pE{V!Pk9k>^o+&mG@clU%OoAyIf;Abr(}a{Rf=eA)+ee7Wh1t$)k* z(bW#EoFj^Z_hLR5TKLIK#7+#(wq6Jx_9m|jxgT3nt_lbI*f2)exOt!mnJ?Zb`Po7_ z^{r=$vFyn1V0aIHBB#zPWDLTgdSB7=0c3|{6+ZVf?0MncY{sFvxRiQS$J49_)~s~bA0?^7ABBD6)_9L_Y0u*D9@aH) z>=bV}J(kDDpS#9{Y+ex@7pm-4F>NAv@qKX2>&HO)p}F1fcl=orVN{Bo)cl|A!skC3 zvt*WO1;c{-@m<)IzxKb`PjKX!<}t08XsaWxt#7Z690aNMyY$F*>JjdWHd$vi8sMXE zh?%bS&(Rw@=N!>_T3VBNw9L7H#Af)~s?i;4fWKj8aK=qqB#A!O`_gJ_l8tuh3m)Db zz0t|Np>ZfaMDYhIznuNu7W_<~z%KbAG^#ac*EmbTh&Y0I$_05B<;vhWtKcQ8ZMdY_ zaS)S{OOgN6g>sLIhQAI3>Pu33=` zf0>YH92iNwMWZkFqUJ$-s+_*I5>goM#gGk34f( zFu-|B!15*P%HEq78i?<;Vj6>f*B_JpJ6ndLPkYbLc4g>7{2p`B=Vhz%-g$EHo(93n zsYml-*?yDG|HSVrZMt!ux21J_V}UbS^RI}Nxqg#T7dBDk$9!7`Wk!zEfB8v%j8BYc zx!t}B53`Whr~8_mxSg2BYRBJXC}ri>WAno$!yI`UJm1w>M%XZG!Dre47vzIDi1M<7 z30CrT(p7E#h36XouPEm{Q~vn$2sy@*$|PIouv}lcQfNuvYHga!oF(dl;=`X;2YJEltb;t%(;3$^`twbVWukm&x{n)8 zq=~cynrQVaCUC*?vh7KSkbO^h6<=$Hx5nd3t?P_X>yVz|eM4-z*kP@{_)K)ML;T01 ziyqJg>#b48c6)S@9x>@>g{8a|M&!4|slB}?FZ4%Sx6F(f-O`2ge1JngJad&a5 zUaVj_{vP1}Qp3Uj9kU($-|=8`2mS|{OX2@_C@cIIOx|(vS@#}3AGYy1J(A(JooVAR zaecYwvcFP?XB#Lpd~Iu?`Gw$FnSJ%yKHcKK-g-D-_-d04e{Bx?g%&(p=NR0I-#xm* zNbTh*?yef2`$Nc=Vc@RkC%=SsI`)mo`}o+eGKcLef@clG9y%93np0#Pm<>%_T}0kI z*6bGdAK+i#8~^$`{Odp9n_2kR`yOohLD4y@r(S|@{iWDj@Y7tp(Y$1z=3{P=+D>hI z1^Q4QZa&VLEM@?IyA+OAso14Hz7M}tA15R4h{>9JP8gZ7GZ$KzjEq$IQpPrS zlW`qqqRoo}zl(mfjd(=t6uP&}+9k+UauYtISf5I4Mp5u)SVnNH>e2UkjL-GEJ~by@ zb+G(?&KgX5m~=e&ksZ%zpikB@=ea)OK3W&;DoLq3SQ6QGux##=*7q(6S=ai)Y1tj?0_kU0=DIl>sPAWn!%u{7;<;L`n{1^nvTKW|-= zk4^!eHHsdjYuCWzwz~fl^}$__3Clg9jEF|kCfQ;NRW8e>RFW;38R*o4bN5uDNM`IOFPK@6-l88+so=s;y zN2jAdCpoeCrZJJ2ct7V7vmPZ!gK0Ri`6ZI^26`C!gYdlw-0G$ep;F(1=WlfT@D;8e zfAV;e#-i_A`2O>Z2R!LCp5dp)lZ5`2g8r3?uW4`mocdr_=!@Rhk67QNkj>XOUm98u zoYq%e8mg~)bXQ61Zw}^T=eKw$VL`R~&G>{SiK4&t|_G+(fF zh1PnKmwWJWv7p68%<+f;M(w>1M9ED$|KqE$gQmANb6@7WFBUKtK9<9IOt}TY z5+lGOJWl^*NA>t5`31^`*}*h?U6x0S_%Pjdu0*UF@pB_pvfxSts}fg(LJyI4AvJ)dj?j z@oXPD{t^0|4h-ZcESQ}w*mfza^9r>OeT4G4KNdcd8{FJ7PWdAX%qwHjVLigF(*xln zHyZ6fWbRe}Z0bLXtzkF$3>R)T3rl@wXgOn58x36XwX|~}4r z4&8V6sc#m$-_&u%?>toR%d8dNZ=z3XXCm!1)6U8JuLWl0=FM+oMkQA1MZDo_gbOzcAXa< zAU|l0c=oJJJ14AS)P2llu51|EXPs>n#&0nS!TY6oU%xN}k6!v|h~rBts{#E^C1{w)!0VS72wbFl_c4W8@WwE!v~5b$Pdr4;AyR^mXst z@=CQ~^bh&4p_CdqZ+9x6U1EL=fCdTEqmO0 z=c+4?)Y>CZ}Gi`pX`tnc4?Rqj3>UrB|9w^05TkpHS z6TiuG>$0fpD0O{^AFbl^q?4;(vp-X#r|~B=9xCnlpD!yD~w7<_;oP9ruub2J# z$S13|W-#VV;H$Qm0Y6}PKs>d^cXv1APLFKUc-?R7v0+*!M`n{7PI`c^cTvNhz9wfE zur4+Ggpx9X4S&n>HS`QLjsH8jd`ih@^1T(lhS`sq4L1|dzi^K+U;YZERS%CVt*RNf z@Md43rwg1Zr%o04P~3ERXznM@T0}Ucxy2@J^WWcbzOV9fp19o)pG<;Z_JD78&SxrG zw~~xn_}l@}fb^q$&dw?T2gg29vtrt6V_C*O?%Qp}@7paIzO`4yW6}6W2iN#Yjw|Q4 z^noMnJ80gIQJ3-zO6E$ADW9xzoqVG6yrl=R_D;tJemI%9By!~`KkFaC1N4bq;5EJ* zM!IsM*Z2n3<}5%b{)QRO$}={cT|gWV`zx)(f*S(kjSa@8s&OCl?M;3&l;c0UO*pRl zWq&b|7ixP6FgT1Hd!PA)(XZc@8d{Hj_O=pbW^IxkxYr%m$&tN>>^E#DB|F_bb-fcyH=S-U^ z=`)OQnq{ezJ>N^=8jK4F!hr%aI^UBq+pPr3TEr$?8F z2PWvxn~=}6XY0$gv?rbYKCWI~GA|FA`X#+#7C4cpFRQQ8_kZMm>-DvqzB>AT_qNv{ zYv!)D*TwX00y?W~ua$qlZ+F4T`*u%&Hd^=I_xxaE(s5{BeT{4dp5^d53%=snWG&_= z(I$(XTD0-G?6Du6fUdE}e!x|>VuA&sXCL}dN6sE^omaw~(T~O(u)*BT^?KlPYH!M> z%k)S#u&}^WgV^F@%=#kGccWs! zQn9_V8*3kC4|DLiJtvuwh4z)T!>81zcD?!V_8#j`%OlnO93;|CnP5EJ@hzOrSj&*x z$P3#pkL{Pe9m$=J{VdUw@c#+&+=%aJ4X5|lQ&#J)LCl4osUGHcz4%RE`dSLDOtQyC z{y(nW<@MBeX%A}uAuaM}=D!)9EuDh(>pIyb-=R*;&#C+go$U3_^Ta#_*kxRMz4OjQ zN0(i)_G!m1se1Coi)?%S>EBn{Yc}V3ym!Yp;+Yuv_-TBJ-x%&Un;CysJCERA2P}+=ypRR%N>|ra^mh$BvCQd*@VR}Hjh~(EERUW| zPEW?$NFB?3SvFlQdLU}?{k%=Us$RHInH9)Bb1A`iJuXz3fH3OY} zs8yfTTkz$1tLa0cT}Qa`A^WY})mzG1{sZK9V);QyrY*ZPCtjUF<{+Co1kVM)sZ}OpU(8u?{`5!Xp;=`8%<2&tpd>Xj#s)KpD*9+)*^3C}(u^Y{O zfom`FqYgoLVPEyg^rW!xFpB(DnU;x02;b{v;^+TDdGSST$Bp(o{_khbHAd>B4{4G9 zv@6)@nb)t>gnk9M7asJoubGjTfx{cXUwdEO<(F~h#EdMUTpf5Jd|D5jy2~rWoG17K zIr&=>bEi0b&3XPxs`#37FTR$ZkXJ@DK)#Ms`CAszSJ4mrG@j> z)|`8Q*(vuw zPh!KEQA+HN=s|J{p3}Hm_gZ5DYwdad@OAdEPx1B)+rC~V{m+)+;_)5!>_-Qh>8fX` zTgM~1Qhy!wCYGz=Y^KEj>bK@KllF8~{afAo?*lf%mvLP6PIRlQ`gpfne<|a+lY9BI zj02bK_wIOZbNiyP&EZ~k6;RjZlVXXGL(=_I?I=UM1wnwL1V-XXi4cOG8-{8tXXsvgm&BfC%k z-otT~>*wOQ-L{S6V`5R)-_7P}@YWh|NpvgS&*PJcdBnd#eub~QITkc7mCbhH_zvT3 z2G35@tMa3iae6drgaE*6MO-iShq&~?1kbZkf-n6_N7ah zSPpaHAlWTnmS)b#Dn-w*a|NSESv=EThqG3cKV5o6ICIxcM))pU&+1rrdF$Sqm$hD! z{U$wfI7WY~)n7mB`;N1iWUmvh)+pYq@=GDL5&b4Q6SilIHKiBdb-`NnaFyBAQAY1o z&MR{+WfoyWtwKJ$0?)5O{!BpDBhT(u9$?D#xA7_?@&L~s);t5Z*i0X`kB&A%2GTIOYA$^_GkwXO;(CSgb3Q^P|q zF>VJYPVVOC_^*EJ`~mfEIoEicIg;%WT$7%-XSNY~Oyv^(FXw;sY4#DpyRr+6@EjXY zu44YLWGoZWU5fG7GEc?msc!tImka+UF>c{<;u!8`43a@(b$w^mu8O>k2XjaMcjj#>-KYj_HVSM~|WZM1Nss13lG@s}cOQrX|&+Y5dl zU;bVn<3OqG3+!P?uSn#Zv6)w5Cq|d`J2v7keeBp;JU-eF8WTPw*6~$awxvZHfz!Y9 z-a?K^Rus{O<{!Nx>r874(TD0o)-7FDl^0s>%DQG`nq3d}ga!{ z@QbUU-_C0tn{L>Tj?Pw06|nUip?RkGL+81<`cQnv8tRf<^WI6{F_Jh33EwL3nPnL5 zPq3!7@c}yfsf!Q7d+9#)hNJr+t7pjXKytM#1@2*c)6L%pxw{e`l55QN1Ikt1)de7Ad zZ_IbOV=lAD-1)mj>`{&2ERX#70%yOC{W$Hn-K_mK_BHS9q=%$M0>D6X6~mVVys%}g z{K5WqRA&!X?8*cWy*^;rYZo6f$)PS8Cm;VfbNw^w)V`4D>P~FZ27Rx`h_x{N7uXowSIQvjYz?ppXkbG3QAY9@0pdQF^TPd8DS#P8I$=yjU=O*+>z$r-_5 zwQajPGu@#Vd~Vy-K;oo4{QoT56@FEpX}j`hxO%n2x3(tGaH9Pz!FF{@*BQjPgB`Z3 z9f`Szy|vR9wOwUGCuhQs+q>Y!TGuw$9bN>x!HXZEFMLtF_!Dh@QM`E7{pJy_XNr}# zpf6%FmS+2%J&C|VXHO!}*^|HrD=RbrTFC4jY|n?6m5@_JGA}3Y>^lVEonDMfVvgSA zPmJ`}@t?5umM(Qz*lx3klgr?|I14_SvzE2B)>-5^?o>DU9b-?c5I&j@?{0-IhLQ_m z3jL8!^Av3IRv!8bJoFx$ccw=kWvp7~ym@fJZuu*U_bzjJZ&NqCw}A5Ey%~HXTIy~r z_hp~O9TZ4>v$KG?p25!SWEpjVY7AVx49x1nqaUJ^%DNWQ#Wj>Ap7;KjOs-KLSmOx0W$i*e%=71FrV^ zs9XDi!-q3=2E*d{@|CN!*M;Lc_&WO)%@dJDUo{HTp?_?}OYeNl3jG%QqOIq`gIi{6 z4>&DS^#(Kt{^E;rzyx22@hoJ{M$0ZBn{89P*BHqyPcO9Fs_$qk5idNuLgGAXT=v<# zzT7dBlJdu}Z&WxTWFDx7KA^E(LzOFGLGsy?7o<*~n_6+!r1MkGE;u*%SS@mF?oWD# z%IBBgi>>Kio!xXf{jvD||9I{{tB%hx_cvqj`n+?WPQNFBkDH;}65v|U8tqy#{Iy9DE5=CbBe94 zy{A2=-hRVW=J6Wl^J?aG3Tyw#_?9}cdsiHD&I&8!D!ZCs(zQ+lIjK4i05`40R7P;@ z$NZ+a|6jCaQ${388TsxwWelBn4?NN$|3&@6v8Q<7aM)P(EWgCHmUK7a_Ma)&6;D46 zw_nxypnTi)8xOaCPkU!SC#F<|yO5xJ7Mk1F^&`%0&`KZv>T zbXcwRJUrd=%TL!qgY2K;lbvPveNA?#J~m$$w(?aUXK3A!9$ACEwvlqsaKx-} zVtcRNa(X+4xAIN%6^D1LVLEx%6G@y`KsyShoL$4_97;O58!p91rNp#V_l*? zT~0aeMU-A(hUYN9+8Ze!+)bP_{WaNFzVcA_dh|atW*nam>#!<|cc+&HVRd zZvl9Y)P9OPF5!7P(Pm&V!5AcJBEo8MM)LuOUDkcTY3J3&H9B zy!Yxi80!i85d6Y$>##$6>%4EllRbOmVftr5b5Z6~YiARF}tkZHG^z@=E>g}fIT_=>&&-?n8-RTsZ9+LqnmHuO!K zXSuSok#;3F5^bHS=q00oKR7yV8FSp#r>X@XEX|FTmleu(eX6|kb1Hal$)Bbq6^4J$g6jNJ0`^dO9psj8p)U}c=0JYiM>~R;?#PLvTJF7J>z}?n@m@@ zHt@dQ9a}woLpdVUCwsnJzqMCfzcuyg`h-4ZxqXrkL1Lf!(x)jQ8FfcP{V0$9vh3 z-b{G!&2=d__if&5E%2)QUU>e=5l075ed^zF{!37THs9HC4H}D`TfxsdYnBm;#N+EG6KftG_T{k6b7%QN@u$ht zg6>R;G7!HwN66@qtivv_oQ!D({ItRWP6YZ2 zX`iM3f)DjN|3~$(zf(#*`sP1up6KWqzu>*+!{Yh+Nw$06)PZOEMtoHJz{44v-mAY` zX`_bt%8t6zBkSC{&jk(_2o765JBQI!zR}vE3|ZF7T4DlyZFX}QwK6AO4x>EVPt}Sj zuA+rahF5V_OsgqBCBD&j_@vr; zWRyP3FQBwz|7u-g4x^3i-8DnYf>{N+P?-xK*-CRcfn2Qn6hx(yDp;OFIf3BoI;vxSAP3Ya}_S*Ew-)%Ug*|;Pe z7A=T>H1~7(N1{xIckJ-il+(fIGM6`(!J8kU{TA8~4STX${73Dk@-8>QNBmX(3)i2Z zU!pYIY$e18_Xlrr0@`;c?MpG6*}%*gY!dzAQ8)$=6fBtul! z*QiVN9HX8r>QNl4-Y=9b2e>EN%C(oT{g4y4zigie_7Zye{A6S2znzs+ zDb;(Ospg!gnt7_OiFRGtk)QrG7#3cTS0l^IZLr$+8Rs`w_u8o#IO*<3{Ql71G=KQt z?g)lASlCX%JNXu=%^bxvX5h1q&bv3w3P*iKgD$7AjfMWVf_b8wtVm6t;#kdaojt1aec3}~4HTU^ zF+2x*^Gw=rrhH0yd8`1N%|hld(Qw)~e#zLSMkagEjbgv(FnPPjw7Gqr2CMnlXA^nc}&yJKM%bDZ#Kf~5M zp%?qL@aAvW@6L%V;@u>kXJa!TMZDC<#Jt$P(O*N}WDcyY>veyF(ZguCiTJ?d)E%Qf z#b?%mM`gXPv}KXTVAit6t@iD#Mi-cUSs?Tjw7@;~{Tp7AKSJ(>#*7!p%d+zIG0({)B6bJ2HWB@z6qSvU-io}leY_R zupJ18cM$_BALjOWbuaR8bgVPG^2j61jrE&g_~<`^;g9DQhh?i>(%(31GO@su2c!<= zoWSjqiDgr6g%tJ>dy|W%ja=VzPQEvGXgoez_qTrV-q=;9Ieap4qWb@X|9o%kd;H%A zJ?LEX-@G<+><_5#!@$51dM{n=_1?x=OL(`0=Sv3k87iAf{e_od%k=4NOhkvp<=3u~6OmpgNn7jWYT`;s%R zpFLmEYQ7cBfY)`*QxiIq=4=no;=oV5csj6AyLIU9tL?M;#x~S3*Q>#UuVvD)6AOSDx_WE|JjZrRMtfsCtJljiqbycV1L%si6M+YZ=dmU}Qafp5j zH>ty@Y#L`)YCX}j0ljdCQDf4qDMTU!XnFhyBj$3;Rgw%d@;2%R5`=y;MOI7M*X+-Roe3KaXEFS`Hl7Gy6dZe4oz z5cNyn_PNi5^TP2u_@L&tnr|dyjx&C>cgUr$Mwh-WpugVuL}wM$ryL>57x)|9qhGXe zUe`Xpmv3=myqRUt7P%WsKO=s)3VB>`a)A>+{LzBvi-{kG7uxHFt@NWD-N_=){;Bw3 z`9de=s?(Y4XE0K4wm?hT&Q({Jf1_2A+q^h5i2Phl6*+`|{LUS;36yY_RD z|9g2KXD_e2y03QaVLzkqg|2@>f$N_jo6&C*>K*9Xo}^c3-B7_d8rL0+D<9m-nE$0% z2|7-`YY&^?`7coRJ=%Ypu|H|s!#cxX_OPw)_`si2_Ausb-7y!=tqJz9kkhAiXRtf2 zQ=d2^b$OKbyz{!FF0A7T_ORd4rxNC~W~vc>;wEz3p5eS>2dB^t-EZrdr)T(9cCdla zkN4giZ!YiG@?LhZvl8BW^IQrIujGAz_sQ;i*}KL;3!+)oCq6Tl|I+o3afXv2I<4;B zH(YB`{0ADP@4(X|C7csy9hf<`g!4<_F` zaOQpY5_{iYa%VbN0lImkeA`dKDx6ye@k=I;Uy#_xedO`W zD~pcv^7suSkKaY)@gpzb;+fs{S0D9GeN^4L4}kBJ zl$VchlNskQ!>YZte9qWs@%fZ4Fo9fvDsTIxvuB=f*TESX;HcoV0a{8N=O3AyAod=O zabXoP9Eu&=G{!zZQu0xHVpqM;+k^1-AKrZ5kzwBd9sJjvdN6x|HiQp98-|=tg zYKv<7=*+gLa^B@8*rE!UKhgF53B{r*w}e z+7D3O!jt7To}@>BJ2v(#8&6axW3ubq#~6f1&vISQn7if{uyOs(VEB60R(DgE#z1bU zkaE0vH;2gI(APTkr_k{{8rUCq?$MCwnEfw#2;9$^|`^c;lKu_~;;0^6_JrOeyF_ zrc}=PVlrisFR*dvfJ_md*7&SijbkXj)bIg&9KW6ZrSNM1 z*)g5pd;11wk4X5Zm?POhi{P<$^)$mJ$YA4FP2)?6#p^mY;Yp(0`4V`B3~LmPcrcm- zOf2FV@3!$SJ#s!}JbA=<24TtjvIh*-i5>YHMzuMzN3`yJm!-Oa_n)wbbjKHL9(AtE zqq>Q=eTwhu-1um8Wjludx6r-lvq>>G#2=J=!T9JFe6bYwIpayKU7UI}9?Q;c;`~pH zuaw-B_5U!wydS?r9=n78($5}cygvnw zUH8wtct?vhNEAOx&&Sf%wHhzB<80)oo|uo&e)q?VL%)aKynKW=vrgJeJKx9e(|(_H zi4*g)|B?{CnW4SVq+)iy!~51Q`55z{0qYF;2rCr>&`mx<`}t?&Bealn0el}{>0ylS zI&Q%;iao=NaR9bEJ}brnyY`OarO-KJVZSA`g0V!g@%*2}I53WNpB3XknH^_}aj?Jt z95D{3%NobmC?6O}J_)Zh&o#*0?qtFh$&!DaWr9uDl=r^B92^rqA0}^~V6W%e&$@_j zZ5iS}{d^0_Jkj3^K*uSA9yL#8Hr}U4-u%GfOS#b0uV^C|-lBak z4l}mtB2i|0d#=i{Ifd^eHzsivZODII?MBJ-U4XqdOg%;q&i}CcpC0)=b?bdIb8A39 z(vx1<=jbSc)5p{!n))5(w(z_={sV4B3}9eV?=r^sxW)t@N*q%)-y5!v<$pI18u7#z z=Rr>squ}u)Czhl891Vj1RrI}-IXV;kV>5qm>yYBv+8_Lc^|0bD1wX-J_p$i;|9U^Z z{BQg;H+#5hZiG{U?HXV#xX%T?&a<@0FKoDHM7~Aa-{ZObu20Xm==g)giEWE7bmi6j z;*-r68Yj!>cXXmzh-6`3!U&Z!F9mrcLQ(uFY<{w}j~!p=!~gcX$Uw$7>d18L;jBYz zh>ft*s{W4&%)5 zSn9!}NIWOl-+>1{ZMmNUkKq3RJigoo9wR>|Jo>xv*ln51T8C`7?F9R2$8YiB)9YtG zl9-ns=&zjjRc(gIQNQ!=DMtHIe-HL5lgO{>kF>L&tvyiD|IeB8pU)l=dK{QH`_CbE z%#Ot^T*>qMw+snszv~~^k!GPAmDy#^wt1*{A9kmuM(%m|Vh7u`|D~(^1mg=W{N#Kg z6PUi-3mwGY|E0P%bmR+5cgduAgAhg#u*KM}rG@9x_S~W|p;B_CmR3DFPI*t8r&o-xto-q~BYa;`Sv79oGe-NqcxE{IhS6R^ zc?-K)>)e6i_9173v%LzN@2nV~!yY0$c1APb{3quoMOPtD$*;*C;|A8_wrwW5mYmt} z*m`ILnrd%;_!|6}tcGYC|Jj>ses~K1dp5LGUya{;FU}O3#Q!AzPv(D*hIn;SVRNsq z@;tS{-oLn=To%2q;(s#lze2ebu2*smG-zL=wCa^{bybVTMfVzZ3@iD8pSQ5ZC=XS0 zPQ~~W*kdxFr&o|2TR#liXIhDmlp&`|k~s$%UcZoW<=tV-_$F{Z#NJDp)l^7US! zXYB3hSs6BjyIDKEa~AqCb!hxn|ADpHjDG;*mmN{#f4kTSz30nnYsQZGM_+KmpJ*%R z4PzdAe>1S5&kJH3&B-$6RbYSB^I2REPv}*6d@V3q>Fj~#u{O)2jhx+-Lm$fNWzI8H zKF754RUf{hS7CIo*0}%jyvCmRZ4vwTvcn%9)49$%+SmE0hcE5?d=qCwXm9WEh|bSV zo@=geX0HFB?}l}G&pwD^q_jS^vaQJ4MYG3eCe&CSHUl@$9`X z8wc9)VX)59eg{6J^dXQH9NW6t7>SNyuYqpx0e|){a)GCOC=Lw+f5+gbLVlT++HZvG z|8>c(vOfQMu>RTC4n}YL@ZeCx+LralV7UH+pX{o6wc%j?)vq6n{^b3GB|I;<`jK5> zo`v6k^+&0^8>DeeET`IXGp>s|TN1^5MZLM#{EV4=y-Z$9(Io*$?O37Ta#7 zY-10ww&b;~+sfHry_EZcP20CkuH5!`^xwB`8)hcg&N2IJo7?cpw${ow9&ccOa3g1g z48gX@dGR}!u&!Lfv)7V?k8NZOvr~*2AI`b;rANW*{bum_9AED?Ju79tK){-4?VjBx9BKiUuM`_Q$zPN1osaFoc@OKm z{l31n{pQ?y?*aCh`VHQ>qK~gn5%pL6Tjk0`Sw?+wm> z?Gt$wT9K{qpQ&^6r_`x^Vby&by6A6cZ$Ib2_Z|Gkir3A)k&T>xvx)sE;h@g)^>Faw z1RSjIf`j48*B)0N^WAzs$nL)0qU?XJUiJ!Ym^(K3?6#tz4!nN{yaRsOJhE+@$1_ja zb7k8+G#|2gv=$gMZlc_WjBx;ce6=Mtf?fX9*hEvJF>ibt+cS)3C}Y3cOx-pK8k=)& za91tkFM}TSJeA+1O+2sM{&)@L^(;ltD7(1w)yKbRIs04cD`+R5_0=3A7bKEf=-5Hz#20#Aua!2Ejb<&;EYD$s`2Sp6B)a^ZWgg z*Eh58d+oK?T6^ua*V>kU{Epop?VTM>Km+~6o^CnUN)~yB+`nMqUB|2k3NG+-$B@CA-nF1N6~vB~Kl0jsWp#e)_>bqT`i>8} zQ}fj}+2i4R58Jb^wz>9O+j?_W`M7=DWwa9~26pg);00F0MU3N|v+|Fnk59Z9N{aU$ zm|*i#!wZN7vM0SHI+i;?GZ*BR+~byc*XH;6?+-7mIA#$q+m17BZIJwe9@>m2HE$a9 zY$`fiHx{+O&(yjAW3E`(GSN@G>aZ_#T zu_twsp(5cj}h?$QF~aft9==8Dq$=v4;*5V5jw&c%XelB{&lNj5rYB z-d;G6@8ZCMP8^uCaNTCTAz{{7=DUENrbGP(gvaP-xyq4}O zcq2ZU;X{d1TLxc4S)KPdxAT6dIQd$dXT@NCD+X0+Iq4<*q^rH{l*wV7UGiBg=_3B= z<7|I9dvHKjoni^-e4o?HQVf}hi6H^L9$1X8Mq)8keEgl=weYb?(K{#C(6`*%4!+i4lQFgD<;H=hZb^d^^wQ zZCPSWv9ZyrMuv6A!!7X4#~4p&o_!BxNS?oh=ygLVB&ljIvH7OIgZtz{*rZ`$*>=Wl=pSS?KZfxs` zZEvl`zeGgsyz#gdk*!=a<4j|WuAMac#`1=m@;1S+nX|ua69p&Z^8omy(a-I?(Rhcm zR$_XInb%z8`U9iDqYI0Zn!hFcLv$XeF&4GeLb<8n)VQ>(g1ET-Ip>-g=ePJHYnkhz++t)V{6Byj1%;vHNWRmEB~*Kjf7L?HmkT`uBAMt0mIABX zd;FEy%;@`){^eXn(h~ne{^efVlJe<0xfaMRg-lQLtPKrJj%nYn@_tj+9Kzqy3 zT;NRq>+=JFxn{0zKk`?ycGyi*F5;y(0S8^3=Vm-`cAmVT-J?E$r6<_Urx3eWb_Qh<<9_ zwC3TFk?dPTSEgI|BBQt z>v)j4Bo=~u+z|>#6ARM{MGJ**Zv71%^|R{=92#ckw&)x)CVax$?c6z~khLA}C$714 z>IufTD$xI6?#bbPjiZ`eI*0HQD7NkOUH>w6VH zzj^?ALu7;u;F1(gn4d1&_~rK7w(+`uSnglb&)7aE&dtKE!E0=vrJrnR&yLnZBSnJ_ z|GfRt4YL+SpY_db>!6X`H(UkohA*{k#7tYn22=bb(THH4xkp)NMW7R&s!5YLQ}?un zM$B!2eQXv5FO%6?#-zj+XG6j(>-ddn#23UsP-E~BFj(8302y%|S! z|Bg9n9u3Sxb8>0wX6Ey&S+_A+AQc-?RASM=I2;rfq7(;H_OOa z`M_587q$#%+h0VHdAr;6eOCJm^|zG%q66&yay@o`YFl(jHhod>)na_I>8ryAL$cXv z{kz)q9iEjHJ;eOP*ZTazzVFoi*>@Os8T>J$PiTK8cL&$u!!pB@u|0o4M%^e6w$o|W zRZoLI!kvYlw8%8@D*S$X`Hdd$&iBEkb%D38XbOa`$Xix_OMoAzT`ba`3q?8 zde2$UKSOz?^>WHTLtl2Dwef`Vc}la(YixP6e}PjT{rSf8X}=F`olpDctNpo3>)ZGJ zw)F9`ZSOwztB$5KhM|m^@gGfR9)0`TX{SHmW&Gr6EMGL`&t%Mu-=y_*(sG~gGXB)^ z12pDtW;za=2?_Be_L)9D(EE-BZuB zHi%BF_YG)!*T>uG$`J((zJbq2YX-JyFDE>lgVL<^#;zWJHL|_eo@zfgGul4iN`Azh z#gSF$+HcB^*0LXtaz`OsyDyibC*m*H!&^sa%Q1A~k6qgy&Bx}x8Jee9NYa_h79gK~ zZMk8H{To^!pKU5L!RuL}*fGM>xBrQJi|kdF2A!*!6>WK{JUWWLI_L{LLgm9l@k5La zs@d1gS&^J8@HyiJzHkmUS!HvSHVZny}ap(PWBLI zkoP*bz~9B!2U1qDNk_lzlcA&q2iZPMkWsq$FhN!_K1^B}L-=g`m>@S4FpdRzR@Ag> z#*iI52CU;xq(`#N8)I+ea^5CzcgS0+uD)cBUR%O@Q_ct@Uc6p+zNzClVy~BxhjsU7 z?Xgxo*ln!;Qa`-EkbJB%2hZgVQM0w;-frdIrd)kWx%Vk^XSXtcp$zv<9c{f$nQv1@ zcgPjUUjBbTa~)eO-J3`++>v*8MkLyiH!Su7ZAr%L$WN);B|WtsV>iC^Eqr})mw64a zjL}XRFqiFdJAM&#w%}g;L%p3TCEa;Dv;5vrKiK(j_A|U}F+Tmeu|@X{RHXD-HSlGS*~LN*k`7K}UVgtqEinoii_? ztYE1&BukopnP&}ev1amHIIXliCr@Wm{XW8PJ4xyg*qu{7}8`anON3^P*Gg*J~@d03Lc@vzM%)2{eS950K zO!-ylmCisx<6FWTnZ@{r)6NcH`v?9sH;>b<_MqVA&Ls}LY_n*eI1%il2^!aUujZ2- z+fTY;_-Q=aQ~%<(?v3?3QOcb#cZ<@GNRBUJ47!(omi|n6=F41|Z&vK5)LTpXzy0Rv zUw855>8?}1d3yh@Q@(lX^R{#rA#?6TS5cl|-EQv5zZ0^|nmFU;&C_bWJ=sEOetv8i zv_4_(#v7QQxnI|B-ZpE#6(iA;gBOzhGkRC;3KRrZ`OygwJNg$@z^vjN`Qi<1{~={> z{(EV3Ffe~?o^2zoxBIfy4>Qg|f4nE!!2PuP67GnPHPENt1L*fs>K}w}RqzftVal8s zYo^NT+1A_j@#*i?+5RoG=e`pd#5k<`Dx>>6z=6K>PC)(GW66$PM_HZ0&*M8z>szCk z#eVEl0@$ZyV4rfLeRTo%;0u7u1oU$G`Jw1E{-4f0??Uvjqtf74{9l0nBA>UYcY{xU z)@CSTVH)*unE zHn6v{tFc>eV!f6u`C6N4=dKLX&QEFM;C|Y8YD%=V^|I(z3KDB$8x^{og_~D-;$itQ(sXm4g6ZWnEx&+!#Kikl6X@3jmlB1lo z^>eT%(79B(KTT@ktK&qR(O1(f}OWJI>HKvFp)MG_Y6vLkHKziMP^>4?hcCmi!1c69>oY zn@La=`G99L3b?-&#xX}^IH5BHO%+yY_IHX z?lz0zmwAoYnSoblLqA;J79U|nkn7klS+OvDS2U#7Tlq`bh}5&sD}DIdXI`Qian|u+ z%7}MqZ4xQ)nc>vE1DG0|V$Ia%bkc=W-!qz0>&TW(Z!w?54>Pb^Z}<)PFCXO3 zlUAYq1kMVV)_`kvd3@ZHC%)#s(UMGiq8+jPzu4_RK>4bU_H#`8<@{D`p0(ZkVGaGP z7cd>DJ*2uP8^`93adfpmmaoe0Hhs*2_?f3gUP87?5I; z##hx59@=*Qb{kpHW9gPt>C%4Y6@+G)F?$9^UP8X}^ERSPeumJs#ECm)_6~Z|_y}(_ z4u5$T8Nr6Py?zbv*fzVVXA$6HY);SR-Ii<)-F>k^ADRoZD+p+NuV53#wv17)KKR#vmVCb9V)+3xd^*$;L+z#!! zCt3?!D-3-j)(dM<+rgW2Vvhq4!<(T^3Ce^in`+Z7o8lkhPrQ4Y@KWz{gtwi%IVbiY z?YVEW4l$1S_bQ{T^NJ(zV8ff~M`KYxHIx;uU&i-~%=JV(|7`a1H<@vE#aFGH=4;PC zEA~y(XHG1iZ};~JdY1wGQ{Q7O&&$6cvIjaLxW(spi@gvW2aee95^m-VljJpYa9v_A zfJ^HOoY)J^tVhC$z0iQKEyZ4_@N~vrn9I4Q`@4iNdjOqk_QA}~7z?J{r+9xL`eVwj z_eM& zQ902o?Q7k~SnP3q)fc)9^cW^4Np}5wYv(NZXZ?NmL?5r2)28^yqUCx^4b3-u#bf-k zT9Nm_celSa*mH8eGd{n#W8C5T{K`7XUdR`%IsfEuj_#vh_stQ!ZHN5b{>so9=iSf0 z@#e^-JrkA+f9-ZUX^;K;WN(h1`ry;q`-{%mzH?7}b969{?=${_oJn22@ocl&&5nJW z`QG_=$+@!APe(@S^o8igyZW^BI)pFR?)3T<=+w#J*pNc62X)YE8y+sq-mN`3%oImZ z_6kc`AAHoJ7t1odLpG}k;^#K|O0Y>rHi9O9k@uc9PgUIVoLH&Omy~yXDIT7Ed51s6 z&Ah?VIo$(XB!8&<1j_3i+(3-66KOKBaq+Jq_~xGkmhKxaU`cGKF1!z3*aPqD0hZsf zVcFw5xKDtkdGq$@r+}pm$82Lq-xC~<5ho=Tj{DfNCxhd+960{`Z=G=b2jvCFYfb{k zr4Ah5ppN_}w#`XLX7+=Z_~*PcgEs+Q-k5vw!ED?Ae_D7p_5jawfak5q{5{1?Ple~; z4&1c;+Jc(~o}~t!c3S6twto)-Hz$CbQSnZLo3<<<*#^A?alL+reZmraf7&vHkt@N| zU&H%s_+qyT?)|U>zMZ}f+vb3)TkJP0gB=)OV_q%{wFYg_dXkU;$7smt}wRYD_PdT+f%^z8{Obr{0Z>g z#2Y5jf{*{IaoF%3>%jNPlfd^+_}CCFI9u>7{WpE$V8>B#{5z-C58)&3RA-a#zV+$P zCPOtp^h1OBrk+cLi+*Hr_Cpun5J6&{OFyKup8KX@-2NnEgrozUYK&7V@aKbtz>Fm{TBVB_+3Key%uL z;K|r_+SpL^dhFoqUfI5FEqcFaJehS-?4p-pQ@#Dy+qa!Z-u;wYik)@l=RDDus$YFL zgR@fixs~vSC39kNRP9|@QPGB7`?k(@`_`?a-L*}xnszgPvwhn$wEHq;US0djwmwUC zJnTn5EEwrMMdm~g?O0L4YV%?FZmHUp$)5|WJxW~{JcOr7C#%rIsoqp@F4dp^ROzSi=ih*T81b7ZvE3Wc z2S|>a%wD(oaHsD;`G+Zdt5fH5#b0c?8{$LWmr6c1bqOgtWrgfEoEQMQ6L9P6&ei49|M5M`_^R7FPw^G)_Qjw5(6zU8-_QP*Kh)H9ILO@C z^QT;g&l;!fy*~p}pZEd(|L^ogk8i;ag>mLHSI&Iv3i{|g7-+5H+*iih`xjggk-yXy z-pOudy%Xt9ObH)ne&#P6kUkCA$bM^G!Ihl1&|$Rj{r%IIuqz9{&{h{X2O4z~6hubox7x} zD7?hB%LCp?$qGS#=y+ByCktrjk0YU#FH4r8j8LiWaq( z>fRw8J+U8saR3=O0~t6Iyur7N&qw?c+o!YeXAiamq5-QJcZj!m!u=R?-@{jDAIXu= zJj2+toV%i7%;&!1=*yH3o^F@F4g5K>CohsT`*(0G=)^Fs%}9;yVy6#r_KtjjRYT(> z7pc5u_ODYHoOXStY5#7eUG$fct%hC=ie))#rgPuM?}eg|BTI1au-x2VFlOCdyg_>! zbK6_^h{pDz0lIncbYda2hj$<49q!q{H<^w;G@~SH>gvvdy1KJ?(W#sKpg4Mzx@OH- zN7cWY`p>g2?d*@c_-g#`0gEH(s1-w0{?^paql{7gml>N0YzCexF*Xw$E(VU=3CI?t zjk-s`iG$QV3T;XFhK=-B=frNqPs+{c{uJz&P0!th+Zj5Zee(#KT>~ zEpJ*YR{XUyM$AHf8(lAOt>AyrF>~Gn-;h~Xh==!KJWA`1qot%PzZc(6Bco5CoMd#- zAXhf8<$qW9zA!00tsMv0ra4{DD)t%BzBiC;a}(>_6F+$6Z@{-_xyJU#0E~l-CD_SN zdxUYVBiAeiZ)**m=#23w9boUjb@G6rZ`!bP^;zw}STyiAjPXVCq|Z8fZ7*=SH~4-M zy%uYh)2lR@lVh!KaOhKm`h7w)Q}~?|yHIl*720?Ny0Mj5$Z_u2;=byl#JRwgyPzY` zpM2SM=0NYzZ9b9(UBNefi1^;(8L#|PXAIeG4T}94zbT}Tm0mNKaRvu=(B9>uy=@Nd zH9kd=owR4d&YlUGJ!AX3T!a5!-DQkmzldI_yy%SI_%O16r^a)4X|&2c`mW$)m+>1K z-5HNP6$V>V>2(S`_A-x2ivIvi8h}T0P4Njl(C|of!Filhrm;D020km8-y>#z*&V){ zw)RTpEq1zedVb)w3AxN3x1GMgjN7TpxCh@cuC2k54HLSnBNkVM$3LM=c2m`k-1LOk zmYd%4*|E5o$G^Z6@#xC0Sy9~^Cs-r#V|R@fu}0Q`_HpjrY8d`LH-*0sqK@#a5&2f~ zhr1SyH8$;PFf!(eb7|zvvje(O`DNZ#N`?SFIv)r}9tX!#^^k3Iz5s2$tV17(Y`uKL zp23kdPVBON|FB~M$@lWzf3fwtq8HuUT}Hd27fI&i)t>N4rsz%E>Ff(X`yv28K&N&m zas~P%`z+(=)#%eZpftH>u(iI7_3*>H%PjWB7VV9!*aF~)esJTFT-&BVdaNqNl0)}W z3*Q?KoNlEJ>DK;g)_qW{%8@CMds5=X+3BhL&R%yry_Neu>0nJ=v+fhyuq!ZU0>Msi zUmukn4D{#7{NiYWdy=?sb5Rv*_E?6^e^EFRV@CXMI`f^zyr(k%GT?AI{15*x$Wx+! z_S|w}`#!ekmSk=T&KKS1roHw<+H5ua#U7jZxr+;v99*#Z`HAxjf(zSCUFnmC#I9p~ zs^I0=Fc=#O7fvVgOuMa*Qu-S1^i^SG2Rr|CXAf0&$Y*x?W8Gjm?2b#@DjZmraergh znENanu6nmOEOw`00c^#$i8WbX;n0N&;3+zy@n6FDi4(tGejYm3sz>@|kHdx2fhD3} zc3LO>vVWfpAFk~-hrv!fFv*w+XKbt)@|Haxdz{j}<}jbZ`22&v9<%qwsqkUpU)l#7 z{4E(0X=2|sayL2pZEMA5?jws>yVZZDjr+L!y3eyVBpDzy$d&={EpB9hDr5lmN;Q1` z=0CFk*bfg;NBrL0&jG6hJVkWC5^bk!J$uU7vHc1DZeW=eo67zcz3~I%0i<`o^ZW}w zn;Mb-wf|2>XMsn`emjvirPA4c2ELtiHY?*TVLj(@? z;!_-%UmBI3OnE<59`}*L<7T6G!>-9Gv(?6xeOtksHSpVF>YxkgkX`oCXFd2WdM~?z zGVz!nUJ!{Q-^mtWHf4{LOpJ`DUIO`wGehTjkG{oU3Rqi>mHH2*_J`bDj-Q&bt2d8r z4RW3k++4p{%;ThfpwI=S`hi-WS2W)Ef!f2Ee)3=Kk5;91`o-#)3-%VjVJ>lGb@>?{ z#Ga9_;WA`a|J*F&YgqkDr=Wd#C1wBPqxNw(zhUc}Zrj6o!fS0zjk~0~h@;m+L!J8w zKlRbmY&^{Rx;72ZykT@H#TLe>wajxOF_KI-eb?kvO zCd1>PQ?+vCVXF9yiWArRK+rNpw?0RrXYx!IL z7cN;k7xEkb6EhneTxb9n8ign5uUwr|mT(AKNZ$B^UnvH^jK16G_D)lt_&WHthBZ~b z)>~y#+pOk|iOxQU>3b-)slaQO>_0lz2wC}R<`Qz=4V((!ZvC2V+jYBa%uL+Y%bD{R zm^-$9bEdE^rSLZUo+u%_>_HDv!?-nX;brPLi}54$M#>saVm(}+$?Iw>NsSSpHN}Q5iCsZ5qnh`Jo{a5tuoF5{G&uR1{pWy`qGV6l$lSe;+KGH>7`wD(hyE3YpM+Ju8gz|$&d3HqVsi9ugT<|ae?^MDhrK8kZp=T_6B7I6ZNn28}T(6C;o65hn;TxbDOlF(ysaq z>x2LQQgnqD@<%XK*uZ?*A0PU2Y}l<~?KNlj-2LPs>>(LTn3(qNSmeKpa$V}prJhBb zKcswWKiWS-sGo4g>9*VOvE80OXcekn9rfC`Sm>S=pTLekJ0@5X^`UbvhR@`^e4eqN zn8)4KUe2^(Yw{b7=-Yz8$Mq>neIqAReqG+v*ck(tJ>>6&?xmIqTe)wX%noflW7+CW zi{vzIYAXjWD@uk(9t9qPtI_+D4-MVNI+pA1u$et%;Ma6f#7`Z`OL^ex<;0u!pEe~K z=s!jF)%ybd(~Ped59LQ9Z~56TuW=9C(JpxH?=vM?1|1X~;XTEQt;5qyn|@EucJ;Xs zeB97M-TY5gXDz2-q*`~0V+8QV@j_?mls^;SP~+e*Hx!*~7R z>#TA+bL`y%4E$~F>t6(ay4U%~8z-*+Ea1Nw_}^^c@7Pf~@UOz2R`Ac`+a3PJ!2g}i zjvWZNqVWU2UxZ1Q?d^iTX^P{up+e{KCjPj$tSCt{^t)mZYo;=T(vrDDL;JNKRLCAnary|xPmvbJf~29NlvgXh0cIdDcXZ@+BT*%?3UoM21=)_O8y zf`*mDKX{XwmTX~60oJg9F%>YTK(RfhK+cq;DU*|yoa~HAHj3_;4nqetrc@l)8VqI) zp6me*%}3;y$y#nz4T@YxUCA}>?{D%uh+JdlL7VTq`SGLO=UT=%^T03duiKcX>}3X0 zZvcO8y*H_+`Fr83nWVeCIRShE7H<&1w+xSE?C;c;yH^Y^Wv_^51p~!Li-G^w3~#sN z3HaC-8sA~)t$2oeFAokZ_c!M__Tg5vVT6T!xHp>U2Tt?9)=qUx`TI^Kb+?>o_lr(6 z7O%9T#k7?d8XSrHh@Ja5cmy08;UCXp%TGV+{AJ8utM2dp&`QDeL%wS0XG_fx^?2B+>qXH4hw?TLSRxg(J6nQIq$__o}OwKqpn-fvZH%;o*R z2Ojb|G<%bGQ0wCtKe95!K9n5xGG%nusK;i(BA+uv>lSDeih;UA$QrfCX%=O5hs}AC zctw{)3@$tPO&#MOZYpIaRQqG8dBXi4fJ={nH(NV!$;SPy9k@iCZZp5^*l51D_11lq zc>9U=qDBsi4??$=6I%uz%NOTi@KrR?-A6&Et+SlAbY6T*F^_8$-`iWx;{A+TC~KJ zJ8hOfaC%3%0A&l#>pdn)SyN|k?!xmsu8dgOD9 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

)+xdzQ)rQUp+cubiyTZXS9Cd`U~7Q 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 literal 0 HcmV?d00001 diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..6af9106 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,5 @@ +edition = "2021" +max_width = 100 +use_small_heuristics = "Max" +imports_granularity = "Module" +group_imports = "StdExternalCrate" \ No newline at end of file diff --git a/src/core/error.rs b/src/core/error.rs new file mode 100644 index 0000000..4cada1f --- /dev/null +++ b/src/core/error.rs @@ -0,0 +1,91 @@ +//! Error types for RaptorBT. + +use thiserror::Error; + +/// Result type alias for RaptorBT operations. +pub type Result = std::result::Result; + +/// Error types for the backtesting engine. +#[derive(Error, Debug)] +pub enum RaptorError { + /// Data length mismatch between arrays. + #[error("Data length mismatch: expected {expected}, got {actual}")] + LengthMismatch { expected: usize, actual: usize }, + + /// Invalid parameter value. + #[error("Invalid parameter: {message}")] + InvalidParameter { message: String }, + + /// Insufficient data for calculation. + #[error("Insufficient data: need at least {required} elements, got {available}")] + InsufficientData { required: usize, available: usize }, + + /// Invalid configuration. + #[error("Invalid configuration: {message}")] + InvalidConfig { message: String }, + + /// Division by zero error. + #[error("Division by zero in {context}")] + DivisionByZero { context: String }, + + /// Empty data error. + #[error("Empty data provided for {context}")] + EmptyData { context: String }, + + /// Invalid index access. + #[error("Index {index} out of bounds for length {length}")] + IndexOutOfBounds { index: usize, length: usize }, + + /// Python conversion error. + #[error("Python conversion error: {message}")] + PythonError { message: String }, +} + +impl RaptorError { + /// Create a length mismatch error. + pub fn length_mismatch(expected: usize, actual: usize) -> Self { + Self::LengthMismatch { expected, actual } + } + + /// Create an invalid parameter error. + pub fn invalid_parameter(message: impl Into) -> Self { + Self::InvalidParameter { + message: message.into(), + } + } + + /// Create an insufficient data error. + pub fn insufficient_data(required: usize, available: usize) -> Self { + Self::InsufficientData { + required, + available, + } + } + + /// Create an invalid config error. + pub fn invalid_config(message: impl Into) -> Self { + Self::InvalidConfig { + message: message.into(), + } + } + + /// Create a division by zero error. + pub fn division_by_zero(context: impl Into) -> Self { + Self::DivisionByZero { + context: context.into(), + } + } + + /// Create an empty data error. + pub fn empty_data(context: impl Into) -> Self { + Self::EmptyData { + context: context.into(), + } + } +} + +impl From for pyo3::PyErr { + fn from(err: RaptorError) -> pyo3::PyErr { + pyo3::exceptions::PyValueError::new_err(err.to_string()) + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..c8ebc70 --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,9 @@ +//! Core types and utilities for RaptorBT. + +pub mod error; +pub mod timeseries; +pub mod types; + +pub use error::{RaptorError, Result}; +pub use timeseries::TimeSeries; +pub use types::*; diff --git a/src/core/timeseries.rs b/src/core/timeseries.rs new file mode 100644 index 0000000..fdf38ed --- /dev/null +++ b/src/core/timeseries.rs @@ -0,0 +1,346 @@ +//! Time-indexed array wrapper for efficient operations. + +use super::types::Timestamp; + +/// A time-indexed series of values. +#[derive(Debug, Clone)] +pub struct TimeSeries { + /// Timestamps for each value. + pub timestamps: Vec, + /// Values. + pub values: Vec, +} + +impl TimeSeries { + /// Create a new time series. + pub fn new(timestamps: Vec, values: Vec) -> Self { + debug_assert_eq!(timestamps.len(), values.len()); + Self { timestamps, values } + } + + /// Create from values only (no timestamps). + pub fn from_values(values: Vec) -> Self { + let timestamps = (0..values.len() as i64).collect(); + Self { timestamps, values } + } + + /// Get the length. + #[inline] + pub fn len(&self) -> usize { + self.values.len() + } + + /// Check if empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + + /// Get value at index. + #[inline] + pub fn get(&self, index: usize) -> Option<&T> { + self.values.get(index) + } + + /// Get timestamp at index. + #[inline] + pub fn get_timestamp(&self, index: usize) -> Option { + self.timestamps.get(index).copied() + } + + /// Get slice of values. + pub fn slice(&self, start: usize, end: usize) -> Self { + Self { + timestamps: self.timestamps[start..end].to_vec(), + values: self.values[start..end].to_vec(), + } + } + + /// Map values to a new type. + pub fn map(&self, f: F) -> TimeSeries + where + F: Fn(&T) -> U, + { + TimeSeries { + timestamps: self.timestamps.clone(), + values: self.values.iter().map(f).collect(), + } + } + + /// Iterator over (timestamp, value) pairs. + pub fn iter(&self) -> impl Iterator { + self.timestamps.iter().copied().zip(self.values.iter()) + } +} + +impl TimeSeries { + /// Create with default values. + pub fn with_default(timestamps: Vec) -> Self { + let len = timestamps.len(); + Self { + timestamps, + values: vec![T::default(); len], + } + } +} + +impl TimeSeries { + /// Create a series filled with NaN. + pub fn with_nan(len: usize) -> Self { + Self { + timestamps: (0..len as i64).collect(), + values: vec![f64::NAN; len], + } + } + + /// Calculate sum of all values. + pub fn sum(&self) -> f64 { + self.values.iter().filter(|v| !v.is_nan()).sum() + } + + /// Calculate mean of all values. + pub fn mean(&self) -> f64 { + let valid: Vec<_> = self.values.iter().filter(|v| !v.is_nan()).collect(); + if valid.is_empty() { + return f64::NAN; + } + valid.iter().copied().sum::() / valid.len() as f64 + } + + /// Calculate standard deviation. + pub fn std(&self) -> f64 { + let mean = self.mean(); + if mean.is_nan() { + return f64::NAN; + } + let valid: Vec<_> = self.values.iter().filter(|v| !v.is_nan()).collect(); + if valid.len() < 2 { + return f64::NAN; + } + let variance = + valid.iter().map(|v| (*v - mean).powi(2)).sum::() / (valid.len() - 1) as f64; + variance.sqrt() + } + + /// Get minimum value. + pub fn min(&self) -> f64 { + self.values + .iter() + .filter(|v| !v.is_nan()) + .copied() + .fold(f64::INFINITY, f64::min) + } + + /// Get maximum value. + pub fn max(&self) -> f64 { + self.values + .iter() + .filter(|v| !v.is_nan()) + .copied() + .fold(f64::NEG_INFINITY, f64::max) + } + + /// Shift values by n positions (positive = shift forward, fill with NaN). + pub fn shift(&self, n: isize) -> Self { + let len = self.values.len(); + let mut result = vec![f64::NAN; len]; + + if n >= 0 { + let n = n as usize; + if n < len { + for i in n..len { + result[i] = self.values[i - n]; + } + } + } else { + let n = (-n) as usize; + if n < len { + for i in 0..len - n { + result[i] = self.values[i + n]; + } + } + } + + Self { + timestamps: self.timestamps.clone(), + values: result, + } + } + + /// Calculate difference from previous value. + pub fn diff(&self) -> Self { + let mut result = vec![f64::NAN; self.values.len()]; + for i in 1..self.values.len() { + if !self.values[i].is_nan() && !self.values[i - 1].is_nan() { + result[i] = self.values[i] - self.values[i - 1]; + } + } + Self { + timestamps: self.timestamps.clone(), + values: result, + } + } + + /// Calculate percentage change from previous value. + pub fn pct_change(&self) -> Self { + let mut result = vec![f64::NAN; self.values.len()]; + for i in 1..self.values.len() { + if !self.values[i].is_nan() && !self.values[i - 1].is_nan() && self.values[i - 1] != 0.0 + { + result[i] = (self.values[i] - self.values[i - 1]) / self.values[i - 1]; + } + } + Self { + timestamps: self.timestamps.clone(), + values: result, + } + } + + /// Apply rolling window function. + pub fn rolling(&self, window: usize, f: F) -> Self + where + F: Fn(&[f64]) -> f64, + { + let mut result = vec![f64::NAN; self.values.len()]; + if window == 0 || window > self.values.len() { + return Self { + timestamps: self.timestamps.clone(), + values: result, + }; + } + + for i in (window - 1)..self.values.len() { + let slice = &self.values[i + 1 - window..=i]; + result[i] = f(slice); + } + + Self { + timestamps: self.timestamps.clone(), + values: result, + } + } + + /// Calculate rolling sum. + pub fn rolling_sum(&self, window: usize) -> Self { + self.rolling(window, |slice| slice.iter().sum()) + } + + /// Calculate rolling mean. + pub fn rolling_mean(&self, window: usize) -> Self { + self.rolling(window, |slice| { + slice.iter().sum::() / slice.len() as f64 + }) + } + + /// Calculate rolling standard deviation. + pub fn rolling_std(&self, window: usize) -> Self { + self.rolling(window, |slice| { + let mean = slice.iter().sum::() / slice.len() as f64; + let variance = + slice.iter().map(|v| (v - mean).powi(2)).sum::() / (slice.len() - 1) as f64; + variance.sqrt() + }) + } + + /// Calculate rolling maximum. + pub fn rolling_max(&self, window: usize) -> Self { + self.rolling(window, |slice| { + slice.iter().copied().fold(f64::NEG_INFINITY, f64::max) + }) + } + + /// Calculate rolling minimum. + pub fn rolling_min(&self, window: usize) -> Self { + self.rolling(window, |slice| { + slice.iter().copied().fold(f64::INFINITY, f64::min) + }) + } +} + +impl TimeSeries { + /// Count true values. + pub fn count_true(&self) -> usize { + self.values.iter().filter(|&&v| v).count() + } + + /// Get indices of true values. + pub fn true_indices(&self) -> Vec { + self.values + .iter() + .enumerate() + .filter_map(|(i, &v)| if v { Some(i) } else { None }) + .collect() + } + + /// Logical AND with another series. + pub fn and(&self, other: &Self) -> Self { + debug_assert_eq!(self.len(), other.len()); + Self { + timestamps: self.timestamps.clone(), + values: self + .values + .iter() + .zip(other.values.iter()) + .map(|(&a, &b)| a && b) + .collect(), + } + } + + /// Logical OR with another series. + pub fn or(&self, other: &Self) -> Self { + debug_assert_eq!(self.len(), other.len()); + Self { + timestamps: self.timestamps.clone(), + values: self + .values + .iter() + .zip(other.values.iter()) + .map(|(&a, &b)| a || b) + .collect(), + } + } + + /// Logical NOT. + pub fn not(&self) -> Self { + Self { + timestamps: self.timestamps.clone(), + values: self.values.iter().map(|&v| !v).collect(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rolling_mean() { + let ts = TimeSeries::from_values(vec![1.0, 2.0, 3.0, 4.0, 5.0]); + let result = ts.rolling_mean(3); + assert!(result.values[0].is_nan()); + assert!(result.values[1].is_nan()); + assert!((result.values[2] - 2.0).abs() < 1e-10); + assert!((result.values[3] - 3.0).abs() < 1e-10); + assert!((result.values[4] - 4.0).abs() < 1e-10); + } + + #[test] + fn test_shift() { + let ts = TimeSeries::from_values(vec![1.0, 2.0, 3.0, 4.0, 5.0]); + let shifted = ts.shift(2); + assert!(shifted.values[0].is_nan()); + assert!(shifted.values[1].is_nan()); + assert!((shifted.values[2] - 1.0).abs() < 1e-10); + assert!((shifted.values[3] - 2.0).abs() < 1e-10); + assert!((shifted.values[4] - 3.0).abs() < 1e-10); + } + + #[test] + fn test_pct_change() { + let ts = TimeSeries::from_values(vec![100.0, 110.0, 99.0]); + let pct = ts.pct_change(); + assert!(pct.values[0].is_nan()); + assert!((pct.values[1] - 0.1).abs() < 1e-10); + assert!((pct.values[2] - (-0.1)).abs() < 1e-10); + } +} diff --git a/src/core/types.rs b/src/core/types.rs new file mode 100644 index 0000000..95e657f --- /dev/null +++ b/src/core/types.rs @@ -0,0 +1,482 @@ +//! Core data types for RaptorBT. + +use serde::{Deserialize, Serialize}; + +/// Type alias for price values. +pub type Price = f64; + +/// Type alias for timestamp values (nanoseconds since epoch). +pub type Timestamp = i64; + +/// Trading direction. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[repr(i8)] +pub enum Direction { + /// Long position (buy to open, sell to close). + Long = 1, + /// Short position (sell to open, buy to close). + Short = -1, +} + +impl Direction { + /// Convert direction to multiplier for P&L calculations. + #[inline] + pub fn multiplier(self) -> f64 { + self as i8 as f64 + } + + /// Create direction from integer. + pub fn from_int(value: i32) -> Option { + match value { + 1 => Some(Direction::Long), + -1 => Some(Direction::Short), + _ => None, + } + } +} + +impl Default for Direction { + fn default() -> Self { + Direction::Long + } +} + +/// OHLCV data for a single bar. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct OhlcvBar { + pub timestamp: Timestamp, + pub open: Price, + pub high: Price, + pub low: Price, + pub close: Price, + pub volume: f64, +} + +/// OHLCV data series. +#[derive(Debug, Clone)] +pub struct OhlcvData { + pub timestamps: Vec, + pub open: Vec, + pub high: Vec, + pub low: Vec, + pub close: Vec, + pub volume: Vec, +} + +impl OhlcvData { + /// Create new OHLCV data from vectors. + pub fn new( + timestamps: Vec, + open: Vec, + high: Vec, + low: Vec, + close: Vec, + volume: Vec, + ) -> Self { + Self { + timestamps, + open, + high, + low, + close, + volume, + } + } + + /// Get the number of bars. + #[inline] + pub fn len(&self) -> usize { + self.close.len() + } + + /// Check if empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.close.is_empty() + } + + /// Get a single bar at index. + pub fn get_bar(&self, index: usize) -> Option { + if index >= self.len() { + return None; + } + Some(OhlcvBar { + timestamp: self.timestamps[index], + open: self.open[index], + high: self.high[index], + low: self.low[index], + close: self.close[index], + volume: self.volume[index], + }) + } +} + +/// Compiled trading signals from strategy. +#[derive(Debug, Clone)] +pub struct CompiledSignals { + /// Symbol identifier. + pub symbol: String, + /// Entry signals (true = enter position). + pub entries: Vec, + /// Exit signals (true = exit position). + pub exits: Vec, + /// Optional position sizes (fraction of capital). + pub position_sizes: Option>, + /// Trading direction. + pub direction: Direction, + /// Weight for portfolio allocation. + pub weight: f64, +} + +impl CompiledSignals { + /// Create new compiled signals. + pub fn new( + symbol: String, + entries: Vec, + exits: Vec, + direction: Direction, + weight: f64, + ) -> Self { + Self { + symbol, + entries, + exits, + position_sizes: None, + direction, + weight, + } + } + + /// Set position sizes. + pub fn with_position_sizes(mut self, sizes: Vec) -> Self { + self.position_sizes = Some(sizes); + self + } + + /// Get the number of bars. + #[inline] + pub fn len(&self) -> usize { + self.entries.len() + } + + /// Check if empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } +} + +/// A single executed trade. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Trade { + /// Trade identifier. + pub id: u64, + /// Symbol traded. + pub symbol: String, + /// Entry bar index. + pub entry_idx: usize, + /// Exit bar index. + pub exit_idx: usize, + /// Entry price. + pub entry_price: Price, + /// Exit price. + pub exit_price: Price, + /// Position size (number of shares/contracts). + pub size: f64, + /// Trading direction. + pub direction: Direction, + /// Realized profit/loss. + pub pnl: f64, + /// Return percentage. + pub return_pct: f64, + /// Entry timestamp. + pub entry_time: Timestamp, + /// Exit timestamp. + pub exit_time: Timestamp, + /// Fees paid. + pub fees: f64, + /// Exit reason. + pub exit_reason: ExitReason, +} + +impl Trade { + /// Check if trade was profitable. + #[inline] + pub fn is_winning(&self) -> bool { + self.pnl > 0.0 + } + + /// Get holding period in bars. + #[inline] + pub fn holding_period(&self) -> usize { + self.exit_idx - self.entry_idx + } +} + +/// Reason for exiting a trade. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ExitReason { + /// Normal exit signal. + Signal, + /// Stop-loss hit. + StopLoss, + /// Take-profit hit. + TakeProfit, + /// Trailing stop hit. + TrailingStop, + /// End of data. + EndOfData, +} + +/// Backtest configuration. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BacktestConfig { + /// Initial capital. + pub initial_capital: f64, + /// Transaction fees as fraction (0.001 = 0.1%). + pub fees: f64, + /// Slippage as fraction. + pub slippage: f64, + /// Stop-loss configuration. + pub stop: StopConfig, + /// Take-profit configuration. + pub target: TargetConfig, + /// Whether to execute on bar close. + pub upon_bar_close: bool, +} + +impl Default for BacktestConfig { + fn default() -> Self { + Self { + initial_capital: 100_000.0, + fees: 0.001, + slippage: 0.0, + stop: StopConfig::None, + target: TargetConfig::None, + upon_bar_close: true, + } + } +} + +/// Stop-loss configuration. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum StopConfig { + /// No stop-loss. + None, + /// Fixed percentage stop. + Fixed { percent: f64 }, + /// ATR-based stop. + Atr { multiplier: f64, period: usize }, + /// Trailing stop. + Trailing { percent: f64 }, +} + +/// Take-profit configuration. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum TargetConfig { + /// No take-profit. + None, + /// Fixed percentage target. + Fixed { percent: f64 }, + /// ATR-based target. + Atr { multiplier: f64, period: usize }, + /// Risk-reward ratio target. + RiskReward { ratio: f64 }, +} + +/// Backtest metrics. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct BacktestMetrics { + /// Total return percentage. + pub total_return_pct: f64, + /// Sharpe ratio (annualized). + pub sharpe_ratio: f64, + /// Sortino ratio (annualized). + pub sortino_ratio: f64, + /// Calmar ratio. + pub calmar_ratio: f64, + /// Omega ratio. + pub omega_ratio: f64, + /// Maximum drawdown percentage. + pub max_drawdown_pct: f64, + /// Maximum drawdown duration in bars. + pub max_drawdown_duration: usize, + /// Win rate percentage. + pub win_rate_pct: f64, + /// Profit factor. + pub profit_factor: f64, + /// Expectancy (average expected profit per trade). + pub expectancy: f64, + /// System Quality Number (SQN). + pub sqn: f64, + /// Total number of trades. + pub total_trades: usize, + /// Number of closed trades. + pub total_closed_trades: usize, + /// Number of open trades at end. + pub total_open_trades: usize, + /// PnL of open trades. + pub open_trade_pnl: f64, + /// Number of winning trades. + pub winning_trades: usize, + /// Number of losing trades. + pub losing_trades: usize, + /// Starting portfolio value. + pub start_value: f64, + /// Ending portfolio value. + pub end_value: f64, + /// Total fees paid. + pub total_fees_paid: f64, + /// Best trade return percentage. + pub best_trade_pct: f64, + /// Worst trade return percentage. + pub worst_trade_pct: f64, + /// Average trade return percentage. + pub avg_trade_return_pct: f64, + /// Average winning trade return percentage. + pub avg_win_pct: f64, + /// Average losing trade return percentage. + pub avg_loss_pct: f64, + /// Average winning trade duration in bars. + pub avg_winning_duration: f64, + /// Average losing trade duration in bars. + pub avg_losing_duration: f64, + /// Maximum consecutive wins. + pub max_consecutive_wins: usize, + /// Maximum consecutive losses. + pub max_consecutive_losses: usize, + /// Average holding period in bars. + pub avg_holding_period: f64, + /// Exposure time percentage (time in market). + pub exposure_pct: f64, +} + +/// Complete backtest result. +#[derive(Debug, Clone)] +pub struct BacktestResult { + /// Computed metrics. + pub metrics: BacktestMetrics, + /// Equity curve (portfolio value over time). + pub equity_curve: Vec, + /// Drawdown curve (drawdown percentage over time). + pub drawdown_curve: Vec, + /// List of executed trades. + pub trades: Vec, + /// Daily returns. + pub returns: Vec, +} + +impl BacktestResult { + /// Create a new backtest result. + pub fn new( + metrics: BacktestMetrics, + equity_curve: Vec, + drawdown_curve: Vec, + trades: Vec, + returns: Vec, + ) -> Self { + Self { + metrics, + equity_curve, + drawdown_curve, + trades, + returns, + } + } +} + +/// Position state during backtest. +#[derive(Debug, Clone)] +pub struct Position { + /// Whether position is open. + pub is_open: bool, + /// Entry bar index. + pub entry_idx: usize, + /// Entry price. + pub entry_price: Price, + /// Position size. + pub size: f64, + /// Trading direction. + pub direction: Direction, + /// Current stop price. + pub stop_price: Option, + /// Current target price. + pub target_price: Option, + /// Highest price since entry (for trailing stops). + pub highest_since_entry: Price, + /// Lowest price since entry (for trailing stops). + pub lowest_since_entry: Price, + /// Entry fees (to include in trade PnL like VectorBT). + pub entry_fees: f64, +} + +impl Position { + /// Create a new closed position state. + pub fn new() -> Self { + Self { + is_open: false, + entry_idx: 0, + entry_price: 0.0, + size: 0.0, + direction: Direction::Long, + stop_price: None, + target_price: None, + highest_since_entry: 0.0, + lowest_since_entry: f64::MAX, + entry_fees: 0.0, + } + } + + /// Open a new position. + pub fn open( + &mut self, + idx: usize, + price: Price, + size: f64, + direction: Direction, + stop_price: Option, + target_price: Option, + entry_fees: f64, + ) { + self.is_open = true; + self.entry_idx = idx; + self.entry_price = price; + self.size = size; + self.direction = direction; + self.stop_price = stop_price; + self.target_price = target_price; + self.highest_since_entry = price; + self.lowest_since_entry = price; + self.entry_fees = entry_fees; + } + + /// Close the position. + pub fn close(&mut self) { + self.is_open = false; + } + + /// Update highest/lowest prices for trailing stops. + pub fn update_extremes(&mut self, high: Price, low: Price) { + if high > self.highest_since_entry { + self.highest_since_entry = high; + } + if low < self.lowest_since_entry { + self.lowest_since_entry = low; + } + } + + /// Calculate unrealized P&L at given price. + pub fn unrealized_pnl(&self, current_price: Price) -> f64 { + if !self.is_open { + return 0.0; + } + let price_change = current_price - self.entry_price; + price_change * self.size * self.direction.multiplier() + } +} + +impl Default for Position { + fn default() -> Self { + Self::new() + } +} diff --git a/src/execution/fees.rs b/src/execution/fees.rs new file mode 100644 index 0000000..4f3e926 --- /dev/null +++ b/src/execution/fees.rs @@ -0,0 +1,160 @@ +//! Fee calculation models. + +use crate::core::types::{Direction, Price}; + +/// Fee model for calculating transaction costs. +#[derive(Debug, Clone)] +pub enum FeeModel { + /// No fees. + None, + /// Fixed percentage of trade value. + Percentage(f64), + /// Fixed fee per trade. + Fixed(f64), + /// Per-share/contract fee. + PerShare(f64), + /// Tiered fee structure based on trade value. + Tiered(Vec<(f64, f64)>), // (threshold, rate) + /// Custom fee function (stored as percentage for simplicity). + Custom { base: f64, per_share: f64 }, +} + +impl Default for FeeModel { + fn default() -> Self { + FeeModel::Percentage(0.001) // 0.1% default + } +} + +impl FeeModel { + /// Create a new percentage fee model. + pub fn percentage(rate: f64) -> Self { + FeeModel::Percentage(rate) + } + + /// Create a new fixed fee model. + pub fn fixed(amount: f64) -> Self { + FeeModel::Fixed(amount) + } + + /// Create a new per-share fee model. + pub fn per_share(rate: f64) -> Self { + FeeModel::PerShare(rate) + } + + /// Calculate fee for a trade. + /// + /// # Arguments + /// * `price` - Trade price + /// * `size` - Position size (shares/contracts) + /// * `direction` - Trade direction (for asymmetric fees if needed) + /// + /// # Returns + /// Fee amount + pub fn calculate(&self, price: Price, size: f64, _direction: Direction) -> f64 { + let trade_value = price * size.abs(); + + match self { + FeeModel::None => 0.0, + FeeModel::Percentage(rate) => trade_value * rate, + FeeModel::Fixed(amount) => *amount, + FeeModel::PerShare(rate) => size.abs() * rate, + FeeModel::Tiered(tiers) => { + // Find applicable tier + let mut applicable_rate = 0.0; + for (threshold, rate) in tiers { + if trade_value >= *threshold { + applicable_rate = *rate; + } else { + break; + } + } + trade_value * applicable_rate + } + FeeModel::Custom { base, per_share } => base + size.abs() * per_share, + } + } + + /// Calculate round-trip fees (entry + exit). + pub fn round_trip( + &self, + entry_price: Price, + exit_price: Price, + size: f64, + direction: Direction, + ) -> f64 { + self.calculate(entry_price, size, direction) + self.calculate(exit_price, size, direction) + } +} + +/// Broker-specific fee configurations. +pub struct BrokerFees; + +impl BrokerFees { + /// Interactive Brokers tiered pricing (approximate). + pub fn interactive_brokers() -> FeeModel { + FeeModel::Custom { + base: 1.0, + per_share: 0.005, + } + } + + /// Zero commission broker (like Robinhood). + pub fn zero_commission() -> FeeModel { + FeeModel::None + } + + /// Indian broker (Zerodha-like). + pub fn india_equity() -> FeeModel { + // 0.03% or Rs 20 per trade, whichever is lower + // Simplified as 0.03% + FeeModel::Percentage(0.0003) + } + + /// Crypto exchange (typical). + pub fn crypto_exchange() -> FeeModel { + FeeModel::Percentage(0.001) // 0.1% maker/taker + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_percentage_fee() { + let fee = FeeModel::percentage(0.001); + let result = fee.calculate(100.0, 100.0, Direction::Long); + assert!((result - 10.0).abs() < 1e-10); // 100 * 100 * 0.001 = 10 + } + + #[test] + fn test_fixed_fee() { + let fee = FeeModel::fixed(5.0); + let result = fee.calculate(100.0, 100.0, Direction::Long); + assert!((result - 5.0).abs() < 1e-10); + } + + #[test] + fn test_per_share_fee() { + let fee = FeeModel::per_share(0.01); + let result = fee.calculate(100.0, 100.0, Direction::Long); + assert!((result - 1.0).abs() < 1e-10); // 100 * 0.01 = 1 + } + + #[test] + fn test_round_trip() { + let fee = FeeModel::percentage(0.001); + let result = fee.round_trip(100.0, 110.0, 100.0, Direction::Long); + // Entry: 100 * 100 * 0.001 = 10 + // Exit: 110 * 100 * 0.001 = 11 + // Total: 21 + assert!((result - 21.0).abs() < 1e-10); + } + + #[test] + fn test_no_fee() { + let fee = FeeModel::None; + let result = fee.calculate(100.0, 100.0, Direction::Long); + assert!((result - 0.0).abs() < 1e-10); + } +} diff --git a/src/execution/fill.rs b/src/execution/fill.rs new file mode 100644 index 0000000..9f2b1ba --- /dev/null +++ b/src/execution/fill.rs @@ -0,0 +1,380 @@ +//! Order fill simulation models. + +use crate::core::types::{Direction, OhlcvBar, Price}; + +/// Fill price model determining at what price orders are executed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FillPrice { + /// Execute at close price (end of bar). + Close, + /// Execute at open price (start of next bar). + Open, + /// Execute at OHLC average. + Average, + /// Execute at typical price (H+L+C)/3. + Typical, + /// Execute at VWAP (if available, otherwise typical). + Vwap, + /// Execute at worst price (high for buys, low for sells). + Worst, + /// Execute at best price (low for buys, high for sells). + Best, +} + +impl Default for FillPrice { + fn default() -> Self { + FillPrice::Close + } +} + +impl FillPrice { + /// Get execution price from OHLCV bar. + /// + /// # Arguments + /// * `bar` - OHLCV bar data + /// * `direction` - Trade direction + /// * `is_entry` - Whether this is an entry or exit + /// + /// # Returns + /// Execution price + pub fn get_price(&self, bar: &OhlcvBar, direction: Direction, is_entry: bool) -> Price { + match self { + FillPrice::Close => bar.close, + FillPrice::Open => bar.open, + FillPrice::Average => (bar.open + bar.high + bar.low + bar.close) / 4.0, + FillPrice::Typical => (bar.high + bar.low + bar.close) / 3.0, + FillPrice::Vwap => (bar.high + bar.low + bar.close) / 3.0, // Simplified + FillPrice::Worst => { + // Worst price for the trade + match (direction, is_entry) { + (Direction::Long, true) => bar.high, // Buy high + (Direction::Long, false) => bar.low, // Sell low + (Direction::Short, true) => bar.low, // Short at low (bad) + (Direction::Short, false) => bar.high, // Cover at high (bad) + } + } + FillPrice::Best => { + // Best price for the trade + match (direction, is_entry) { + (Direction::Long, true) => bar.low, // Buy low + (Direction::Long, false) => bar.high, // Sell high + (Direction::Short, true) => bar.high, // Short at high (good) + (Direction::Short, false) => bar.low, // Cover at low (good) + } + } + } + } + + /// Get execution price from separate arrays. + /// + /// # Arguments + /// * `open` - Open price + /// * `high` - High price + /// * `low` - Low price + /// * `close` - Close price + /// * `direction` - Trade direction + /// * `is_entry` - Whether this is an entry or exit + /// + /// # Returns + /// Execution price + pub fn get_price_from_arrays( + &self, + open: Price, + high: Price, + low: Price, + close: Price, + direction: Direction, + is_entry: bool, + ) -> Price { + match self { + FillPrice::Close => close, + FillPrice::Open => open, + FillPrice::Average => (open + high + low + close) / 4.0, + FillPrice::Typical => (high + low + close) / 3.0, + FillPrice::Vwap => (high + low + close) / 3.0, + FillPrice::Worst => match (direction, is_entry) { + (Direction::Long, true) => high, + (Direction::Long, false) => low, + (Direction::Short, true) => low, + (Direction::Short, false) => high, + }, + FillPrice::Best => match (direction, is_entry) { + (Direction::Long, true) => low, + (Direction::Long, false) => high, + (Direction::Short, true) => high, + (Direction::Short, false) => low, + }, + } + } +} + +/// Fill model combining price model with execution rules. +#[derive(Debug, Clone)] +pub struct FillModel { + /// Price model for fills. + pub fill_price: FillPrice, + /// Whether to delay execution to next bar. + pub delay_to_next_bar: bool, + /// Partial fill ratio (1.0 = full fill). + pub fill_ratio: f64, +} + +impl Default for FillModel { + fn default() -> Self { + Self { + fill_price: FillPrice::Close, + delay_to_next_bar: false, + fill_ratio: 1.0, + } + } +} + +impl FillModel { + /// Create a fill model that executes at close. + pub fn at_close() -> Self { + Self { + fill_price: FillPrice::Close, + delay_to_next_bar: false, + fill_ratio: 1.0, + } + } + + /// Create a fill model that executes at next bar's open. + pub fn at_next_open() -> Self { + Self { + fill_price: FillPrice::Open, + delay_to_next_bar: true, + fill_ratio: 1.0, + } + } + + /// Set partial fill ratio. + pub fn with_fill_ratio(mut self, ratio: f64) -> Self { + self.fill_ratio = ratio.clamp(0.0, 1.0); + self + } + + /// Check if a limit order would be filled. + /// + /// # Arguments + /// * `limit_price` - Limit price + /// * `bar` - OHLCV bar + /// * `direction` - Trade direction + /// * `is_entry` - Whether this is an entry or exit + /// + /// # Returns + /// True if order would be filled + pub fn would_fill_limit( + &self, + limit_price: Price, + bar: &OhlcvBar, + direction: Direction, + is_entry: bool, + ) -> bool { + match (direction, is_entry) { + // Long entry: buy at or below limit + (Direction::Long, true) => bar.low <= limit_price, + // Long exit: sell at or above limit + (Direction::Long, false) => bar.high >= limit_price, + // Short entry: sell at or above limit + (Direction::Short, true) => bar.high >= limit_price, + // Short exit: buy at or below limit + (Direction::Short, false) => bar.low <= limit_price, + } + } + + /// Get fill price for a limit order. + /// + /// Returns limit price if filled, None if not filled. + /// + /// # Arguments + /// * `limit_price` - Limit price + /// * `bar` - OHLCV bar + /// * `direction` - Trade direction + /// * `is_entry` - Whether this is an entry or exit + /// + /// # Returns + /// Fill price or None + pub fn get_limit_fill_price( + &self, + limit_price: Price, + bar: &OhlcvBar, + direction: Direction, + is_entry: bool, + ) -> Option { + if self.would_fill_limit(limit_price, bar, direction, is_entry) { + // For limit orders, fill at limit price (or better if gap) + Some(limit_price) + } else { + None + } + } + + /// Check if a stop order would be triggered. + /// + /// # Arguments + /// * `stop_price` - Stop price + /// * `bar` - OHLCV bar + /// * `direction` - Trade direction + /// * `is_entry` - Whether this is an entry or exit + /// + /// # Returns + /// True if stop would be triggered + pub fn would_trigger_stop( + &self, + stop_price: Price, + bar: &OhlcvBar, + direction: Direction, + is_entry: bool, + ) -> bool { + match (direction, is_entry) { + // Long entry stop: buy when price rises to stop + (Direction::Long, true) => bar.high >= stop_price, + // Long exit stop: sell when price falls to stop + (Direction::Long, false) => bar.low <= stop_price, + // Short entry stop: sell when price falls to stop + (Direction::Short, true) => bar.low <= stop_price, + // Short exit stop: buy when price rises to stop + (Direction::Short, false) => bar.high >= stop_price, + } + } + + /// Get fill price for a stop order. + /// + /// Returns fill price if triggered, None if not. + /// Uses worst-case scenario (stop price or worse). + /// + /// # Arguments + /// * `stop_price` - Stop price + /// * `bar` - OHLCV bar + /// * `direction` - Trade direction + /// * `is_entry` - Whether this is an entry or exit + /// + /// # Returns + /// Fill price or None + pub fn get_stop_fill_price( + &self, + stop_price: Price, + bar: &OhlcvBar, + direction: Direction, + is_entry: bool, + ) -> Option { + if !self.would_trigger_stop(stop_price, bar, direction, is_entry) { + return None; + } + + // Check for gap through stop + match (direction, is_entry) { + (Direction::Long, true) => { + // Buy stop: fill at stop or worse (gap up through stop) + if bar.open >= stop_price { + Some(bar.open) // Gap up, fill at open + } else { + Some(stop_price) + } + } + (Direction::Long, false) => { + // Sell stop: fill at stop or worse (gap down through stop) + if bar.open <= stop_price { + Some(bar.open) // Gap down, fill at open + } else { + Some(stop_price) + } + } + (Direction::Short, true) => { + // Short stop: fill at stop or worse (gap down through stop) + if bar.open <= stop_price { + Some(bar.open) + } else { + Some(stop_price) + } + } + (Direction::Short, false) => { + // Cover stop: fill at stop or worse (gap up through stop) + if bar.open >= stop_price { + Some(bar.open) + } else { + Some(stop_price) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_bar() -> OhlcvBar { + OhlcvBar { + timestamp: 0, + open: 100.0, + high: 105.0, + low: 95.0, + close: 102.0, + volume: 1000.0, + } + } + + #[test] + fn test_fill_price_close() { + let bar = test_bar(); + let fp = FillPrice::Close; + assert!((fp.get_price(&bar, Direction::Long, true) - 102.0).abs() < 1e-10); + } + + #[test] + fn test_fill_price_worst() { + let bar = test_bar(); + let fp = FillPrice::Worst; + + // Long entry: high (105) + assert!((fp.get_price(&bar, Direction::Long, true) - 105.0).abs() < 1e-10); + + // Long exit: low (95) + assert!((fp.get_price(&bar, Direction::Long, false) - 95.0).abs() < 1e-10); + } + + #[test] + fn test_limit_fill() { + let fill = FillModel::default(); + let bar = test_bar(); + + // Limit buy at 96 should fill (low is 95) + assert!(fill.would_fill_limit(96.0, &bar, Direction::Long, true)); + + // Limit buy at 94 should not fill (low is 95) + assert!(!fill.would_fill_limit(94.0, &bar, Direction::Long, true)); + } + + #[test] + fn test_stop_fill() { + let fill = FillModel::default(); + let bar = test_bar(); + + // Stop sell at 96 should trigger (low is 95) + assert!(fill.would_trigger_stop(96.0, &bar, Direction::Long, false)); + + // Stop sell at 94 should not trigger (low is 95) + assert!(!fill.would_trigger_stop(94.0, &bar, Direction::Long, false)); + } + + #[test] + fn test_gap_through_stop() { + let fill = FillModel::default(); + + // Gap down through stop + let gap_bar = OhlcvBar { + timestamp: 0, + open: 90.0, // Gap down from stop at 95 + high: 92.0, + low: 88.0, + close: 91.0, + volume: 1000.0, + }; + + let fill_price = fill.get_stop_fill_price(95.0, &gap_bar, Direction::Long, false); + // Should fill at open (90) not stop (95) + assert_eq!(fill_price, Some(90.0)); + } +} diff --git a/src/execution/mod.rs b/src/execution/mod.rs new file mode 100644 index 0000000..261e273 --- /dev/null +++ b/src/execution/mod.rs @@ -0,0 +1,9 @@ +//! Order execution simulation for RaptorBT. + +pub mod fees; +pub mod fill; +pub mod slippage; + +pub use fees::FeeModel; +pub use fill::{FillModel, FillPrice}; +pub use slippage::SlippageModel; diff --git a/src/execution/slippage.rs b/src/execution/slippage.rs new file mode 100644 index 0000000..7332e69 --- /dev/null +++ b/src/execution/slippage.rs @@ -0,0 +1,214 @@ +//! Slippage models for realistic trade execution. + +use crate::core::types::{Direction, Price}; + +/// Slippage model for simulating execution price deviation. +#[derive(Debug, Clone)] +pub enum SlippageModel { + /// No slippage. + None, + /// Fixed percentage slippage. + Percentage(f64), + /// Fixed point slippage. + Fixed(f64), + /// Volume-based slippage (higher volume = lower slippage). + VolumeBased { base: f64, volume_factor: f64 }, + /// Spread-based slippage (uses bid-ask spread). + SpreadBased { half_spread: f64 }, +} + +impl Default for SlippageModel { + fn default() -> Self { + SlippageModel::None + } +} + +impl SlippageModel { + /// Create a new percentage slippage model. + pub fn percentage(rate: f64) -> Self { + SlippageModel::Percentage(rate) + } + + /// Create a new fixed slippage model. + pub fn fixed(points: f64) -> Self { + SlippageModel::Fixed(points) + } + + /// Create a volume-based slippage model. + pub fn volume_based(base: f64, volume_factor: f64) -> Self { + SlippageModel::VolumeBased { + base, + volume_factor, + } + } + + /// Calculate slippage for a trade. + /// + /// For long entries and short exits: slippage is ADDED to price (pay more/receive less) + /// For short entries and long exits: slippage is SUBTRACTED from price + /// + /// # Arguments + /// * `price` - Base execution price + /// * `direction` - Trade direction + /// * `is_entry` - Whether this is an entry or exit + /// * `volume` - Optional volume for volume-based models + /// + /// # Returns + /// Slippage amount (positive = unfavorable) + pub fn calculate( + &self, + price: Price, + direction: Direction, + is_entry: bool, + volume: Option, + ) -> f64 { + let base_slippage = match self { + SlippageModel::None => 0.0, + SlippageModel::Percentage(rate) => price * rate, + SlippageModel::Fixed(points) => *points, + SlippageModel::VolumeBased { + base, + volume_factor, + } => { + if let Some(vol) = volume { + if vol > 0.0 { + base * (1.0 / (1.0 + vol * volume_factor)) + } else { + *base + } + } else { + *base + } + } + SlippageModel::SpreadBased { half_spread } => *half_spread, + }; + + // Determine sign based on trade type + // Long entry: pay higher price (positive slippage) + // Long exit: receive lower price (negative slippage) + // Short entry: receive higher price (negative slippage means worse) + // Short exit: pay higher price + match (direction, is_entry) { + (Direction::Long, true) => base_slippage, // Pay more + (Direction::Long, false) => -base_slippage, // Receive less + (Direction::Short, true) => -base_slippage, // Receive less + (Direction::Short, false) => base_slippage, // Pay more + } + } + + /// Apply slippage to get execution price. + /// + /// # Arguments + /// * `price` - Base price + /// * `direction` - Trade direction + /// * `is_entry` - Whether this is an entry or exit + /// * `volume` - Optional volume for volume-based models + /// + /// # Returns + /// Execution price after slippage + pub fn apply( + &self, + price: Price, + direction: Direction, + is_entry: bool, + volume: Option, + ) -> Price { + price + self.calculate(price, direction, is_entry, volume) + } +} + +/// Market impact model for large orders. +#[derive(Debug, Clone)] +pub struct MarketImpact { + /// Temporary impact coefficient. + pub temporary_impact: f64, + /// Permanent impact coefficient. + pub permanent_impact: f64, + /// Average daily volume for normalization. + pub avg_daily_volume: f64, +} + +impl MarketImpact { + /// Create a new market impact model. + pub fn new(temporary: f64, permanent: f64, adv: f64) -> Self { + Self { + temporary_impact: temporary, + permanent_impact: permanent, + avg_daily_volume: adv, + } + } + + /// Calculate market impact for an order. + /// + /// Uses simplified square-root model: impact = sigma * sqrt(Q / ADV) + /// + /// # Arguments + /// * `order_size` - Number of shares/contracts + /// * `price` - Current price + /// * `volatility` - Price volatility (sigma) + /// + /// # Returns + /// Total market impact in price terms + pub fn calculate(&self, order_size: f64, price: Price, volatility: f64) -> f64 { + if self.avg_daily_volume <= 0.0 { + return 0.0; + } + + let participation_rate = order_size / self.avg_daily_volume; + let sqrt_participation = participation_rate.sqrt(); + + let temporary = self.temporary_impact * volatility * price * sqrt_participation; + let permanent = self.permanent_impact * volatility * price * participation_rate; + + temporary + permanent + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_percentage_slippage() { + let slip = SlippageModel::percentage(0.001); + + // Long entry: pay more + let entry_slip = slip.calculate(100.0, Direction::Long, true, None); + assert!((entry_slip - 0.1).abs() < 1e-10); + + // Long exit: receive less + let exit_slip = slip.calculate(100.0, Direction::Long, false, None); + assert!((exit_slip - (-0.1)).abs() < 1e-10); + } + + #[test] + fn test_apply_slippage() { + let slip = SlippageModel::percentage(0.001); + + // Long entry at 100 should pay 100.1 + let entry_price = slip.apply(100.0, Direction::Long, true, None); + assert!((entry_price - 100.1).abs() < 1e-10); + + // Long exit at 100 should receive 99.9 + let exit_price = slip.apply(100.0, Direction::Long, false, None); + assert!((exit_price - 99.9).abs() < 1e-10); + } + + #[test] + fn test_no_slippage() { + let slip = SlippageModel::None; + let result = slip.apply(100.0, Direction::Long, true, None); + assert!((result - 100.0).abs() < 1e-10); + } + + #[test] + fn test_volume_based_slippage() { + let slip = SlippageModel::volume_based(0.1, 0.0001); + + // High volume should have lower slippage + let high_vol = slip.calculate(100.0, Direction::Long, true, Some(100000.0)); + let low_vol = slip.calculate(100.0, Direction::Long, true, Some(1000.0)); + + assert!(high_vol < low_vol); + } +} diff --git a/src/indicators/mod.rs b/src/indicators/mod.rs new file mode 100644 index 0000000..f22ca22 --- /dev/null +++ b/src/indicators/mod.rs @@ -0,0 +1,16 @@ +//! Technical indicators for RaptorBT. +//! +//! All indicators are implemented as pure functions that take slice inputs +//! and return Vec outputs. NaN values are used for the warmup period. + +pub mod momentum; +pub mod strength; +pub mod trend; +pub mod volatility; +pub mod volume; + +pub use momentum::{macd, rsi, stochastic, MacdResult, StochasticResult}; +pub use strength::adx; +pub use trend::{ema, sma, supertrend, SupertrendResult}; +pub use volatility::{atr, bollinger_bands, BollingerBandsResult}; +pub use volume::{obv, vwap}; diff --git a/src/indicators/momentum.rs b/src/indicators/momentum.rs new file mode 100644 index 0000000..b80e66d --- /dev/null +++ b/src/indicators/momentum.rs @@ -0,0 +1,313 @@ +//! Momentum indicators: RSI, MACD, Stochastic. + +use super::trend::ema; +use crate::core::error::RaptorError; +use crate::core::Result; + +/// Relative Strength Index (RSI). +/// +/// # Arguments +/// * `data` - Price data (typically close prices) +/// * `period` - Lookback period (default: 14) +/// +/// # Returns +/// Vector of RSI values (0-100 scale, NaN for warmup period) +pub fn rsi(data: &[f64], period: usize) -> Result> { + if period == 0 { + return Err(RaptorError::invalid_parameter("RSI period must be > 0")); + } + if data.len() < 2 { + return Ok(vec![f64::NAN; data.len()]); + } + + let n = data.len(); + let mut result = vec![f64::NAN; n]; + + // Calculate price changes + let mut gains = vec![0.0; n]; + let mut losses = vec![0.0; n]; + + for i in 1..n { + let change = data[i] - data[i - 1]; + if change > 0.0 { + gains[i] = change; + } else { + losses[i] = -change; + } + } + + if period >= n { + return Ok(result); + } + + // Calculate initial average gain/loss using SMA + let mut avg_gain: f64 = gains[1..=period].iter().sum::() / period as f64; + let mut avg_loss: f64 = losses[1..=period].iter().sum::() / period as f64; + + // First RSI value + if avg_loss == 0.0 { + result[period] = 100.0; + } else { + let rs = avg_gain / avg_loss; + result[period] = 100.0 - (100.0 / (1.0 + rs)); + } + + // Smoothed moving average for remaining values (Wilder's smoothing) + let alpha = 1.0 / period as f64; + for i in (period + 1)..n { + avg_gain = alpha * gains[i] + (1.0 - alpha) * avg_gain; + avg_loss = alpha * losses[i] + (1.0 - alpha) * avg_loss; + + if avg_loss == 0.0 { + result[i] = 100.0; + } else { + let rs = avg_gain / avg_loss; + result[i] = 100.0 - (100.0 / (1.0 + rs)); + } + } + + Ok(result) +} + +/// MACD result structure. +#[derive(Debug, Clone)] +pub struct MacdResult { + /// MACD line (fast EMA - slow EMA). + pub macd_line: Vec, + /// Signal line (EMA of MACD line). + pub signal_line: Vec, + /// Histogram (MACD line - signal line). + pub histogram: Vec, +} + +/// Moving Average Convergence Divergence (MACD). +/// +/// # Arguments +/// * `data` - Price data (typically close prices) +/// * `fast_period` - Fast EMA period (default: 12) +/// * `slow_period` - Slow EMA period (default: 26) +/// * `signal_period` - Signal line EMA period (default: 9) +/// +/// # Returns +/// MacdResult with MACD line, signal line, and histogram +pub fn macd( + data: &[f64], + fast_period: usize, + slow_period: usize, + signal_period: usize, +) -> Result { + if fast_period == 0 || slow_period == 0 || signal_period == 0 { + return Err(RaptorError::invalid_parameter("MACD periods must be > 0")); + } + if fast_period >= slow_period { + return Err(RaptorError::invalid_parameter( + "MACD fast period must be < slow period", + )); + } + + let n = data.len(); + let mut macd_line = vec![f64::NAN; n]; + let mut signal_line = vec![f64::NAN; n]; + let mut histogram = vec![f64::NAN; n]; + + if slow_period > n { + return Ok(MacdResult { + macd_line, + signal_line, + histogram, + }); + } + + // Calculate fast and slow EMAs + let fast_ema = ema(data, fast_period)?; + let slow_ema = ema(data, slow_period)?; + + // Calculate MACD line + for i in (slow_period - 1)..n { + if !fast_ema[i].is_nan() && !slow_ema[i].is_nan() { + macd_line[i] = fast_ema[i] - slow_ema[i]; + } + } + + // Calculate signal line (EMA of MACD line) + // Need at least signal_period valid MACD values + let signal_start = slow_period - 1 + signal_period - 1; + if signal_start < n { + // Calculate initial signal using SMA of first signal_period MACD values + let mut sum = 0.0; + let mut count = 0; + for i in (slow_period - 1)..=(slow_period - 1 + signal_period - 1) { + if i < n && !macd_line[i].is_nan() { + sum += macd_line[i]; + count += 1; + } + } + if count == signal_period { + let initial_signal = sum / signal_period as f64; + signal_line[signal_start] = initial_signal; + + // EMA for remaining signal values + let alpha = 2.0 / (signal_period as f64 + 1.0); + for i in (signal_start + 1)..n { + if !macd_line[i].is_nan() { + signal_line[i] = alpha * macd_line[i] + (1.0 - alpha) * signal_line[i - 1]; + } + } + } + } + + // Calculate histogram + for i in 0..n { + if !macd_line[i].is_nan() && !signal_line[i].is_nan() { + histogram[i] = macd_line[i] - signal_line[i]; + } + } + + Ok(MacdResult { + macd_line, + signal_line, + histogram, + }) +} + +/// Stochastic oscillator result. +#[derive(Debug, Clone)] +pub struct StochasticResult { + /// %K line (fast stochastic). + pub k: Vec, + /// %D line (slow stochastic, SMA of %K). + pub d: Vec, +} + +/// Stochastic Oscillator. +/// +/// # Arguments +/// * `high` - High prices +/// * `low` - Low prices +/// * `close` - Close prices +/// * `k_period` - %K lookback period (default: 14) +/// * `d_period` - %D smoothing period (default: 3) +/// +/// # Returns +/// StochasticResult with %K and %D lines (0-100 scale) +pub fn stochastic( + high: &[f64], + low: &[f64], + close: &[f64], + k_period: usize, + d_period: usize, +) -> Result { + let n = close.len(); + if n != high.len() || n != low.len() { + return Err(RaptorError::length_mismatch(n, high.len())); + } + if k_period == 0 || d_period == 0 { + return Err(RaptorError::invalid_parameter( + "Stochastic periods must be > 0", + )); + } + + let mut k = vec![f64::NAN; n]; + let mut d = vec![f64::NAN; n]; + + if k_period > n { + return Ok(StochasticResult { k, d }); + } + + // Calculate %K + for i in (k_period - 1)..n { + let start = i + 1 - k_period; + + // Find highest high and lowest low in window + let mut highest_high = f64::NEG_INFINITY; + let mut lowest_low = f64::INFINITY; + for j in start..=i { + if high[j] > highest_high { + highest_high = high[j]; + } + if low[j] < lowest_low { + lowest_low = low[j]; + } + } + + let range = highest_high - lowest_low; + if range > 0.0 { + k[i] = ((close[i] - lowest_low) / range) * 100.0; + } else { + k[i] = 50.0; // Default to middle when range is zero + } + } + + // Calculate %D (SMA of %K) + let d_start = k_period - 1 + d_period - 1; + if d_start < n { + for i in d_start..n { + let start = i + 1 - d_period; + let mut sum = 0.0; + let mut count = 0; + for j in start..=i { + if !k[j].is_nan() { + sum += k[j]; + count += 1; + } + } + if count == d_period { + d[i] = sum / d_period as f64; + } + } + } + + Ok(StochasticResult { k, d }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rsi() { + // Test with simple increasing data + let data = vec![ + 44.0, 44.25, 44.5, 43.75, 44.5, 44.25, 44.0, 44.0, 44.25, 45.0, 45.5, 46.0, 46.5, 47.0, + 47.5, + ]; + let result = rsi(&data, 14).unwrap(); + + // RSI should be valid from index 14 + assert!(result[13].is_nan()); + assert!(!result[14].is_nan()); + assert!(result[14] >= 0.0 && result[14] <= 100.0); + } + + #[test] + fn test_macd() { + let data: Vec = (1..=50).map(|x| x as f64).collect(); + let result = macd(&data, 12, 26, 9).unwrap(); + + // MACD line should be valid from index 25 (slow_period - 1) + assert!(result.macd_line[24].is_nan()); + assert!(!result.macd_line[25].is_nan()); + + // Signal line should be valid later + assert!(result.signal_line[33].is_nan()); + assert!(!result.signal_line[34].is_nan()); + } + + #[test] + fn test_stochastic() { + let high = vec![50.0, 51.0, 52.0, 51.5, 50.5, 51.0, 52.0, 53.0, 52.5, 51.5]; + let low = vec![48.0, 49.0, 50.0, 49.5, 48.5, 49.0, 50.0, 51.0, 50.5, 49.5]; + let close = vec![49.0, 50.0, 51.0, 50.0, 49.0, 50.0, 51.0, 52.0, 51.0, 50.0]; + + let result = stochastic(&high, &low, &close, 5, 3).unwrap(); + + // %K should be valid from index 4 + assert!(result.k[3].is_nan()); + assert!(!result.k[4].is_nan()); + assert!(result.k[4] >= 0.0 && result.k[4] <= 100.0); + + // %D should be valid from index 6 + assert!(result.d[5].is_nan()); + assert!(!result.d[6].is_nan()); + } +} diff --git a/src/indicators/strength.rs b/src/indicators/strength.rs new file mode 100644 index 0000000..1697ff4 --- /dev/null +++ b/src/indicators/strength.rs @@ -0,0 +1,265 @@ +//! Strength indicators: ADX. + +use crate::core::error::RaptorError; +use crate::core::Result; + +/// Average Directional Index (ADX). +/// +/// # Arguments +/// * `high` - High prices +/// * `low` - Low prices +/// * `close` - Close prices +/// * `period` - Lookback period (default: 14) +/// +/// # Returns +/// Vector of ADX values (0-100 scale, NaN for warmup period) +pub fn adx(high: &[f64], low: &[f64], close: &[f64], period: usize) -> Result> { + let n = close.len(); + if n != high.len() || n != low.len() { + return Err(RaptorError::length_mismatch(n, high.len())); + } + if period == 0 { + return Err(RaptorError::invalid_parameter("ADX period must be > 0")); + } + + let mut result = vec![f64::NAN; n]; + + // Need at least 2 * period for meaningful ADX + if 2 * period > n { + return Ok(result); + } + + // Calculate directional movement + let mut plus_dm = vec![0.0; n]; + let mut minus_dm = vec![0.0; n]; + let mut tr = vec![0.0; n]; + + for i in 1..n { + let up_move = high[i] - high[i - 1]; + let down_move = low[i - 1] - low[i]; + + // +DM + if up_move > down_move && up_move > 0.0 { + plus_dm[i] = up_move; + } + + // -DM + if down_move > up_move && down_move > 0.0 { + minus_dm[i] = down_move; + } + + // True Range + let hl = high[i] - low[i]; + let hc = (high[i] - close[i - 1]).abs(); + let lc = (low[i] - close[i - 1]).abs(); + tr[i] = hl.max(hc).max(lc); + } + + // Smooth DM and TR using Wilder's smoothing + let mut smooth_plus_dm = vec![0.0; n]; + let mut smooth_minus_dm = vec![0.0; n]; + let mut smooth_tr = vec![0.0; n]; + + // Initial sums + let sum_plus_dm: f64 = plus_dm[1..=period].iter().sum(); + let sum_minus_dm: f64 = minus_dm[1..=period].iter().sum(); + let sum_tr: f64 = tr[1..=period].iter().sum(); + + smooth_plus_dm[period] = sum_plus_dm; + smooth_minus_dm[period] = sum_minus_dm; + smooth_tr[period] = sum_tr; + + // Wilder's smoothing for remaining values + for i in (period + 1)..n { + smooth_plus_dm[i] = + smooth_plus_dm[i - 1] - (smooth_plus_dm[i - 1] / period as f64) + plus_dm[i]; + smooth_minus_dm[i] = + smooth_minus_dm[i - 1] - (smooth_minus_dm[i - 1] / period as f64) + minus_dm[i]; + smooth_tr[i] = smooth_tr[i - 1] - (smooth_tr[i - 1] / period as f64) + tr[i]; + } + + // Calculate DI+ and DI- + let mut plus_di = vec![0.0; n]; + let mut minus_di = vec![0.0; n]; + let mut dx = vec![0.0; n]; + + for i in period..n { + if smooth_tr[i] > 0.0 { + plus_di[i] = 100.0 * smooth_plus_dm[i] / smooth_tr[i]; + minus_di[i] = 100.0 * smooth_minus_dm[i] / smooth_tr[i]; + + // Calculate DX + let di_sum = plus_di[i] + minus_di[i]; + if di_sum > 0.0 { + dx[i] = 100.0 * (plus_di[i] - minus_di[i]).abs() / di_sum; + } + } + } + + // Calculate ADX (smoothed DX) + let adx_start = 2 * period - 1; + if adx_start < n { + // Initial ADX is average of first 'period' DX values + let initial_adx: f64 = dx[period..=adx_start].iter().sum::() / period as f64; + result[adx_start] = initial_adx; + + // Smooth ADX for remaining values + for i in (adx_start + 1)..n { + result[i] = (result[i - 1] * (period - 1) as f64 + dx[i]) / period as f64; + } + } + + Ok(result) +} + +/// Directional Index result including +DI, -DI, and ADX. +#[derive(Debug, Clone)] +pub struct DirectionalIndexResult { + /// +DI values. + pub plus_di: Vec, + /// -DI values. + pub minus_di: Vec, + /// ADX values. + pub adx: Vec, +} + +/// Full Directional Movement System (DI+, DI-, ADX). +/// +/// # Arguments +/// * `high` - High prices +/// * `low` - Low prices +/// * `close` - Close prices +/// * `period` - Lookback period (default: 14) +/// +/// # Returns +/// DirectionalIndexResult with +DI, -DI, and ADX +pub fn directional_movement( + high: &[f64], + low: &[f64], + close: &[f64], + period: usize, +) -> Result { + let n = close.len(); + if n != high.len() || n != low.len() { + return Err(RaptorError::length_mismatch(n, high.len())); + } + if period == 0 { + return Err(RaptorError::invalid_parameter("Period must be > 0")); + } + + let mut plus_di = vec![f64::NAN; n]; + let mut minus_di = vec![f64::NAN; n]; + let mut adx_values = vec![f64::NAN; n]; + + if 2 * period > n { + return Ok(DirectionalIndexResult { + plus_di, + minus_di, + adx: adx_values, + }); + } + + // Calculate directional movement + let mut plus_dm = vec![0.0; n]; + let mut minus_dm = vec![0.0; n]; + let mut tr = vec![0.0; n]; + + for i in 1..n { + let up_move = high[i] - high[i - 1]; + let down_move = low[i - 1] - low[i]; + + if up_move > down_move && up_move > 0.0 { + plus_dm[i] = up_move; + } + if down_move > up_move && down_move > 0.0 { + minus_dm[i] = down_move; + } + + let hl = high[i] - low[i]; + let hc = (high[i] - close[i - 1]).abs(); + let lc = (low[i] - close[i - 1]).abs(); + tr[i] = hl.max(hc).max(lc); + } + + // Smooth using Wilder's method + let mut smooth_plus_dm: f64 = plus_dm[1..=period].iter().sum(); + let mut smooth_minus_dm: f64 = minus_dm[1..=period].iter().sum(); + let mut smooth_tr: f64 = tr[1..=period].iter().sum(); + + let mut dx = vec![0.0; n]; + + for i in period..n { + if i > period { + smooth_plus_dm = smooth_plus_dm - (smooth_plus_dm / period as f64) + plus_dm[i]; + smooth_minus_dm = smooth_minus_dm - (smooth_minus_dm / period as f64) + minus_dm[i]; + smooth_tr = smooth_tr - (smooth_tr / period as f64) + tr[i]; + } + + if smooth_tr > 0.0 { + plus_di[i] = 100.0 * smooth_plus_dm / smooth_tr; + minus_di[i] = 100.0 * smooth_minus_dm / smooth_tr; + + let di_sum = plus_di[i] + minus_di[i]; + if di_sum > 0.0 { + dx[i] = 100.0 * (plus_di[i] - minus_di[i]).abs() / di_sum; + } + } + } + + // Calculate ADX + let adx_start = 2 * period - 1; + if adx_start < n { + let initial_adx: f64 = dx[period..=adx_start].iter().sum::() / period as f64; + adx_values[adx_start] = initial_adx; + + for i in (adx_start + 1)..n { + adx_values[i] = (adx_values[i - 1] * (period - 1) as f64 + dx[i]) / period as f64; + } + } + + Ok(DirectionalIndexResult { + plus_di, + minus_di, + adx: adx_values, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_adx() { + // Generate some trending data + let n = 50; + let high: Vec = (0..n).map(|i| 100.0 + i as f64 + 2.0).collect(); + let low: Vec = (0..n).map(|i| 100.0 + i as f64 - 2.0).collect(); + let close: Vec = (0..n).map(|i| 100.0 + i as f64).collect(); + + let result = adx(&high, &low, &close, 14).unwrap(); + + // ADX should be valid from index 27 (2 * period - 1) + assert!(result[26].is_nan()); + assert!(!result[27].is_nan()); + + // ADX should be positive and <= 100 + assert!(result[27] >= 0.0 && result[27] <= 100.0); + } + + #[test] + fn test_directional_movement() { + let n = 50; + let high: Vec = (0..n).map(|i| 100.0 + i as f64 + 2.0).collect(); + let low: Vec = (0..n).map(|i| 100.0 + i as f64 - 2.0).collect(); + let close: Vec = (0..n).map(|i| 100.0 + i as f64).collect(); + + let result = directional_movement(&high, &low, &close, 14).unwrap(); + + // Check DI values are valid + assert!(!result.plus_di[20].is_nan()); + assert!(!result.minus_di[20].is_nan()); + + // In an uptrend, +DI should be greater than -DI + assert!(result.plus_di[40] > result.minus_di[40]); + } +} diff --git a/src/indicators/trend.rs b/src/indicators/trend.rs new file mode 100644 index 0000000..d0477b7 --- /dev/null +++ b/src/indicators/trend.rs @@ -0,0 +1,288 @@ +//! Trend indicators: SMA, EMA, Supertrend. + +use crate::core::error::RaptorError; +use crate::core::Result; + +/// Simple Moving Average. +/// +/// # Arguments +/// * `data` - Price data +/// * `period` - Lookback period +/// +/// # Returns +/// Vector of SMA values (NaN for warmup period) +pub fn sma(data: &[f64], period: usize) -> Result> { + if period == 0 { + return Err(RaptorError::invalid_parameter("SMA period must be > 0")); + } + if data.is_empty() { + return Ok(vec![]); + } + + let n = data.len(); + let mut result = vec![f64::NAN; n]; + + if period > n { + return Ok(result); + } + + // Calculate first SMA + let mut sum: f64 = data[..period].iter().sum(); + result[period - 1] = sum / period as f64; + + // Sliding window for remaining values + for i in period..n { + sum = sum - data[i - period] + data[i]; + result[i] = sum / period as f64; + } + + Ok(result) +} + +/// Exponential Moving Average. +/// +/// # Arguments +/// * `data` - Price data +/// * `period` - Lookback period (used to calculate smoothing factor) +/// +/// # Returns +/// Vector of EMA values (NaN for warmup period) +pub fn ema(data: &[f64], period: usize) -> Result> { + if period == 0 { + return Err(RaptorError::invalid_parameter("EMA period must be > 0")); + } + if data.is_empty() { + return Ok(vec![]); + } + + let n = data.len(); + let mut result = vec![f64::NAN; n]; + + if period > n { + return Ok(result); + } + + // Smoothing factor + let alpha = 2.0 / (period as f64 + 1.0); + + // Initialize with SMA of first 'period' values + let initial_sma: f64 = data[..period].iter().sum::() / period as f64; + result[period - 1] = initial_sma; + + // Calculate EMA for remaining values + for i in period..n { + result[i] = alpha * data[i] + (1.0 - alpha) * result[i - 1]; + } + + Ok(result) +} + +/// EMA with custom smoothing factor (internal use). +#[allow(dead_code)] +pub(crate) fn ema_with_alpha(data: &[f64], alpha: f64, initial: f64) -> Vec { + let n = data.len(); + let mut result = vec![f64::NAN; n]; + + if n == 0 { + return result; + } + + result[0] = initial; + for i in 1..n { + if data[i].is_nan() { + result[i] = result[i - 1]; + } else { + result[i] = alpha * data[i] + (1.0 - alpha) * result[i - 1]; + } + } + + result +} + +/// Supertrend indicator result. +#[derive(Debug, Clone)] +pub struct SupertrendResult { + /// Supertrend line values. + pub supertrend: Vec, + /// Direction: 1 = bullish (below price), -1 = bearish (above price). + pub direction: Vec, +} + +/// Supertrend indicator. +/// +/// # Arguments +/// * `high` - High prices +/// * `low` - Low prices +/// * `close` - Close prices +/// * `period` - ATR period +/// * `multiplier` - ATR multiplier +/// +/// # Returns +/// SupertrendResult with supertrend line and direction +pub fn supertrend( + high: &[f64], + low: &[f64], + close: &[f64], + period: usize, + multiplier: f64, +) -> Result { + let n = close.len(); + if n != high.len() || n != low.len() { + return Err(RaptorError::length_mismatch(n, high.len())); + } + if period == 0 { + return Err(RaptorError::invalid_parameter( + "Supertrend period must be > 0", + )); + } + + let mut supertrend = vec![f64::NAN; n]; + let mut direction = vec![0i8; n]; + + if period >= n { + return Ok(SupertrendResult { + supertrend, + direction, + }); + } + + // Calculate ATR + let atr_values = super::volatility::atr(high, low, close, period)?; + + // Calculate basic upper and lower bands + let mut upper_band = vec![f64::NAN; n]; + let mut lower_band = vec![f64::NAN; n]; + + for i in (period - 1)..n { + let hl2 = (high[i] + low[i]) / 2.0; + let atr_val = atr_values[i]; + if !atr_val.is_nan() { + upper_band[i] = hl2 + multiplier * atr_val; + lower_band[i] = hl2 - multiplier * atr_val; + } + } + + // Calculate final bands with carryover logic + let mut final_upper = vec![f64::NAN; n]; + let mut final_lower = vec![f64::NAN; n]; + + for i in (period - 1)..n { + if i == period - 1 { + final_upper[i] = upper_band[i]; + final_lower[i] = lower_band[i]; + } else { + // Final upper band: use lower of current upper or previous final upper + // if previous close was below previous final upper + if !upper_band[i].is_nan() && !final_upper[i - 1].is_nan() { + if close[i - 1] <= final_upper[i - 1] { + final_upper[i] = upper_band[i].min(final_upper[i - 1]); + } else { + final_upper[i] = upper_band[i]; + } + } else { + final_upper[i] = upper_band[i]; + } + + // Final lower band: use higher of current lower or previous final lower + // if previous close was above previous final lower + if !lower_band[i].is_nan() && !final_lower[i - 1].is_nan() { + if close[i - 1] >= final_lower[i - 1] { + final_lower[i] = lower_band[i].max(final_lower[i - 1]); + } else { + final_lower[i] = lower_band[i]; + } + } else { + final_lower[i] = lower_band[i]; + } + } + } + + // Calculate supertrend and direction + for i in (period - 1)..n { + if i == period - 1 { + // Initial direction based on price vs bands + if close[i] <= final_upper[i] { + supertrend[i] = final_upper[i]; + direction[i] = -1; // bearish + } else { + supertrend[i] = final_lower[i]; + direction[i] = 1; // bullish + } + } else { + let _prev_st = supertrend[i - 1]; + let prev_dir = direction[i - 1]; + + if prev_dir == 1 { + // Was bullish + if close[i] < final_lower[i] { + // Switch to bearish + supertrend[i] = final_upper[i]; + direction[i] = -1; + } else { + // Stay bullish + supertrend[i] = final_lower[i]; + direction[i] = 1; + } + } else { + // Was bearish + if close[i] > final_upper[i] { + // Switch to bullish + supertrend[i] = final_lower[i]; + direction[i] = 1; + } else { + // Stay bearish + supertrend[i] = final_upper[i]; + direction[i] = -1; + } + } + } + } + + Ok(SupertrendResult { + supertrend, + direction, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sma() { + let data = vec![1.0, 2.0, 3.0, 4.0, 5.0]; + let result = sma(&data, 3).unwrap(); + assert!(result[0].is_nan()); + assert!(result[1].is_nan()); + assert!((result[2] - 2.0).abs() < 1e-10); + assert!((result[3] - 3.0).abs() < 1e-10); + assert!((result[4] - 4.0).abs() < 1e-10); + } + + #[test] + fn test_ema() { + let data = vec![1.0, 2.0, 3.0, 4.0, 5.0]; + let result = ema(&data, 3).unwrap(); + assert!(result[0].is_nan()); + assert!(result[1].is_nan()); + assert!(!result[2].is_nan()); + assert!(!result[3].is_nan()); + assert!(!result[4].is_nan()); + // EMA should be between min and max of data + assert!(result[4] >= 1.0 && result[4] <= 5.0); + } + + #[test] + fn test_sma_invalid_period() { + let data = vec![1.0, 2.0, 3.0]; + let result = sma(&data, 0); + assert!(result.is_err()); + } + + #[test] + fn test_ema_period_larger_than_data() { + let data = vec![1.0, 2.0, 3.0]; + let result = ema(&data, 10).unwrap(); + assert!(result.iter().all(|v| v.is_nan())); + } +} diff --git a/src/indicators/volatility.rs b/src/indicators/volatility.rs new file mode 100644 index 0000000..4daacbf --- /dev/null +++ b/src/indicators/volatility.rs @@ -0,0 +1,259 @@ +//! Volatility indicators: ATR, Bollinger Bands. + +use super::trend::sma; +use crate::core::error::RaptorError; +use crate::core::Result; + +/// Average True Range (ATR). +/// +/// # Arguments +/// * `high` - High prices +/// * `low` - Low prices +/// * `close` - Close prices +/// * `period` - Lookback period (default: 14) +/// +/// # Returns +/// Vector of ATR values (NaN for warmup period) +pub fn atr(high: &[f64], low: &[f64], close: &[f64], period: usize) -> Result> { + let n = close.len(); + if n != high.len() || n != low.len() { + return Err(RaptorError::length_mismatch(n, high.len())); + } + if period == 0 { + return Err(RaptorError::invalid_parameter("ATR period must be > 0")); + } + + let mut result = vec![f64::NAN; n]; + + if period >= n { + return Ok(result); + } + + // Calculate True Range + let mut tr = vec![0.0; n]; + tr[0] = high[0] - low[0]; // First TR is just high - low + + for i in 1..n { + let hl = high[i] - low[i]; + let hc = (high[i] - close[i - 1]).abs(); + let lc = (low[i] - close[i - 1]).abs(); + tr[i] = hl.max(hc).max(lc); + } + + // Calculate initial ATR using SMA of first 'period' TR values + let initial_atr: f64 = tr[..period].iter().sum::() / period as f64; + result[period - 1] = initial_atr; + + // Use Wilder's smoothing (exponential) for remaining values + let alpha = 1.0 / period as f64; + for i in period..n { + result[i] = alpha * tr[i] + (1.0 - alpha) * result[i - 1]; + } + + Ok(result) +} + +/// True Range calculation (single bar). +#[inline] +pub fn true_range(high: f64, low: f64, prev_close: f64) -> f64 { + let hl = high - low; + let hc = (high - prev_close).abs(); + let lc = (low - prev_close).abs(); + hl.max(hc).max(lc) +} + +/// Bollinger Bands result. +#[derive(Debug, Clone)] +pub struct BollingerBandsResult { + /// Middle band (SMA). + pub middle: Vec, + /// Upper band (SMA + std_dev * multiplier). + pub upper: Vec, + /// Lower band (SMA - std_dev * multiplier). + pub lower: Vec, + /// Bandwidth: (upper - lower) / middle. + pub bandwidth: Vec, + /// %B: (price - lower) / (upper - lower). + pub percent_b: Vec, +} + +/// Bollinger Bands. +/// +/// # Arguments +/// * `data` - Price data (typically close prices) +/// * `period` - Lookback period (default: 20) +/// * `std_dev` - Standard deviation multiplier (default: 2.0) +/// +/// # Returns +/// BollingerBandsResult with middle, upper, lower bands, bandwidth, and %B +pub fn bollinger_bands(data: &[f64], period: usize, std_dev: f64) -> Result { + if period == 0 { + return Err(RaptorError::invalid_parameter( + "Bollinger Bands period must be > 0", + )); + } + if std_dev <= 0.0 { + return Err(RaptorError::invalid_parameter( + "Bollinger Bands std_dev must be > 0", + )); + } + + let n = data.len(); + let mut middle = vec![f64::NAN; n]; + let mut upper = vec![f64::NAN; n]; + let mut lower = vec![f64::NAN; n]; + let mut bandwidth = vec![f64::NAN; n]; + let mut percent_b = vec![f64::NAN; n]; + + if period > n { + return Ok(BollingerBandsResult { + middle, + upper, + lower, + bandwidth, + percent_b, + }); + } + + // Calculate SMA for middle band + middle = sma(data, period)?; + + // Calculate standard deviation and bands + for i in (period - 1)..n { + let mean = middle[i]; + + // Skip if mean is NaN (warmup period) + if mean.is_nan() { + continue; + } + + let start = i + 1 - period; + + // Calculate standard deviation using population variance + let variance: f64 = data[start..=i] + .iter() + .map(|x| (x - mean).powi(2)) + .sum::() + / period as f64; + let std = variance.sqrt(); + + // Calculate bands (std is always non-negative from sqrt) + upper[i] = mean + std_dev * std; + lower[i] = mean - std_dev * std; + + // Calculate bandwidth (as percentage of middle) + if mean.abs() > f64::EPSILON { + bandwidth[i] = (upper[i] - lower[i]) / mean.abs(); + } + + // Calculate %B (position within bands) + let band_width = upper[i] - lower[i]; + if band_width > f64::EPSILON { + percent_b[i] = (data[i] - lower[i]) / band_width; + } + } + + Ok(BollingerBandsResult { + middle, + upper, + lower, + bandwidth, + percent_b, + }) +} + +/// Keltner Channels (ATR-based bands). +/// +/// # Arguments +/// * `high` - High prices +/// * `low` - Low prices +/// * `close` - Close prices +/// * `ema_period` - EMA period for middle band +/// * `atr_period` - ATR period +/// * `multiplier` - ATR multiplier +/// +/// # Returns +/// Tuple of (middle, upper, lower) bands +pub fn keltner_channels( + high: &[f64], + low: &[f64], + close: &[f64], + ema_period: usize, + atr_period: usize, + multiplier: f64, +) -> Result<(Vec, Vec, Vec)> { + let n = close.len(); + if n != high.len() || n != low.len() { + return Err(RaptorError::length_mismatch(n, high.len())); + } + + // Calculate EMA for middle band + let middle = super::trend::ema(close, ema_period)?; + + // Calculate ATR + let atr_values = atr(high, low, close, atr_period)?; + + // Calculate bands + let mut upper = vec![f64::NAN; n]; + let mut lower = vec![f64::NAN; n]; + + for i in 0..n { + if !middle[i].is_nan() && !atr_values[i].is_nan() { + upper[i] = middle[i] + multiplier * atr_values[i]; + lower[i] = middle[i] - multiplier * atr_values[i]; + } + } + + Ok((middle, upper, lower)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_atr() { + let high = vec![50.0, 51.0, 52.0, 51.5, 50.5, 51.0, 52.0, 53.0, 52.5, 51.5]; + let low = vec![48.0, 49.0, 50.0, 49.5, 48.5, 49.0, 50.0, 51.0, 50.5, 49.5]; + let close = vec![49.0, 50.0, 51.0, 50.0, 49.0, 50.0, 51.0, 52.0, 51.0, 50.0]; + + let result = atr(&high, &low, &close, 5).unwrap(); + + // ATR should be valid from index 4 + assert!(result[3].is_nan()); + assert!(!result[4].is_nan()); + assert!(result[4] > 0.0); + } + + #[test] + fn test_bollinger_bands() { + let data: Vec = (1..=30) + .map(|x| x as f64 + (x as f64 * 0.1).sin()) + .collect(); + + let result = bollinger_bands(&data, 20, 2.0).unwrap(); + + // Bands should be valid from index 19 + assert!(result.middle[18].is_nan()); + assert!(!result.middle[19].is_nan()); + + // Upper > Middle > Lower + assert!(result.upper[19] > result.middle[19]); + assert!(result.middle[19] > result.lower[19]); + + // %B should be between 0 and 1 for data within bands + assert!(result.percent_b[19] >= -0.5 && result.percent_b[19] <= 1.5); + } + + #[test] + fn test_true_range() { + // Simple case + assert!((true_range(52.0, 48.0, 50.0) - 4.0).abs() < 1e-10); + + // Gap up case + assert!((true_range(55.0, 53.0, 50.0) - 5.0).abs() < 1e-10); + + // Gap down case + assert!((true_range(48.0, 45.0, 50.0) - 5.0).abs() < 1e-10); + } +} diff --git a/src/indicators/volume.rs b/src/indicators/volume.rs new file mode 100644 index 0000000..cfce796 --- /dev/null +++ b/src/indicators/volume.rs @@ -0,0 +1,332 @@ +//! Volume indicators: VWAP, OBV. + +use crate::core::error::RaptorError; +use crate::core::Result; + +/// Volume Weighted Average Price (VWAP). +/// +/// # Arguments +/// * `high` - High prices +/// * `low` - Low prices +/// * `close` - Close prices +/// * `volume` - Volume data +/// +/// # Returns +/// Vector of VWAP values +pub fn vwap(high: &[f64], low: &[f64], close: &[f64], volume: &[f64]) -> Result> { + let n = close.len(); + if n != high.len() || n != low.len() || n != volume.len() { + return Err(RaptorError::length_mismatch(n, high.len())); + } + + if n == 0 { + return Ok(vec![]); + } + + let mut result = vec![f64::NAN; n]; + let mut cumulative_tp_vol = 0.0; + let mut cumulative_vol = 0.0; + + for i in 0..n { + // Typical price + let tp = (high[i] + low[i] + close[i]) / 3.0; + + cumulative_tp_vol += tp * volume[i]; + cumulative_vol += volume[i]; + + if cumulative_vol > 0.0 { + result[i] = cumulative_tp_vol / cumulative_vol; + } + } + + Ok(result) +} + +/// VWAP with session reset (e.g., daily reset). +/// +/// # Arguments +/// * `high` - High prices +/// * `low` - Low prices +/// * `close` - Close prices +/// * `volume` - Volume data +/// * `session_starts` - Boolean array indicating session start (true = reset VWAP) +/// +/// # Returns +/// Vector of VWAP values with session resets +pub fn vwap_session( + high: &[f64], + low: &[f64], + close: &[f64], + volume: &[f64], + session_starts: &[bool], +) -> Result> { + let n = close.len(); + if n != high.len() || n != low.len() || n != volume.len() || n != session_starts.len() { + return Err(RaptorError::length_mismatch(n, high.len())); + } + + if n == 0 { + return Ok(vec![]); + } + + let mut result = vec![f64::NAN; n]; + let mut cumulative_tp_vol = 0.0; + let mut cumulative_vol = 0.0; + + for i in 0..n { + // Reset on session start + if session_starts[i] { + cumulative_tp_vol = 0.0; + cumulative_vol = 0.0; + } + + // Typical price + let tp = (high[i] + low[i] + close[i]) / 3.0; + + cumulative_tp_vol += tp * volume[i]; + cumulative_vol += volume[i]; + + if cumulative_vol > 0.0 { + result[i] = cumulative_tp_vol / cumulative_vol; + } + } + + Ok(result) +} + +/// On Balance Volume (OBV). +/// +/// # Arguments +/// * `close` - Close prices +/// * `volume` - Volume data +/// +/// # Returns +/// Vector of OBV values +pub fn obv(close: &[f64], volume: &[f64]) -> Result> { + let n = close.len(); + if n != volume.len() { + return Err(RaptorError::length_mismatch(n, volume.len())); + } + + if n == 0 { + return Ok(vec![]); + } + + let mut result = vec![0.0; n]; + result[0] = volume[0]; + + for i in 1..n { + if close[i] > close[i - 1] { + result[i] = result[i - 1] + volume[i]; + } else if close[i] < close[i - 1] { + result[i] = result[i - 1] - volume[i]; + } else { + result[i] = result[i - 1]; + } + } + + Ok(result) +} + +/// Volume Rate of Change. +/// +/// # Arguments +/// * `volume` - Volume data +/// * `period` - Lookback period +/// +/// # Returns +/// Vector of volume rate of change values +pub fn volume_roc(volume: &[f64], period: usize) -> Result> { + if period == 0 { + return Err(RaptorError::invalid_parameter("Period must be > 0")); + } + + let n = volume.len(); + let mut result = vec![f64::NAN; n]; + + if period >= n { + return Ok(result); + } + + for i in period..n { + if volume[i - period] != 0.0 { + result[i] = (volume[i] - volume[i - period]) / volume[i - period] * 100.0; + } + } + + Ok(result) +} + +/// Money Flow Index (volume-weighted RSI). +/// +/// # Arguments +/// * `high` - High prices +/// * `low` - Low prices +/// * `close` - Close prices +/// * `volume` - Volume data +/// * `period` - Lookback period (default: 14) +/// +/// # Returns +/// Vector of MFI values (0-100 scale) +pub fn mfi( + high: &[f64], + low: &[f64], + close: &[f64], + volume: &[f64], + period: usize, +) -> Result> { + let n = close.len(); + if n != high.len() || n != low.len() || n != volume.len() { + return Err(RaptorError::length_mismatch(n, high.len())); + } + if period == 0 { + return Err(RaptorError::invalid_parameter("MFI period must be > 0")); + } + + let mut result = vec![f64::NAN; n]; + + if period >= n { + return Ok(result); + } + + // Calculate typical price and raw money flow + let mut typical_price = vec![0.0; n]; + let mut raw_money_flow = vec![0.0; n]; + + for i in 0..n { + typical_price[i] = (high[i] + low[i] + close[i]) / 3.0; + raw_money_flow[i] = typical_price[i] * volume[i]; + } + + // Calculate MFI for each period + for i in period..n { + let mut positive_flow = 0.0; + let mut negative_flow = 0.0; + + for j in (i - period + 1)..=i { + if typical_price[j] > typical_price[j - 1] { + positive_flow += raw_money_flow[j]; + } else if typical_price[j] < typical_price[j - 1] { + negative_flow += raw_money_flow[j]; + } + } + + if negative_flow == 0.0 { + result[i] = 100.0; + } else { + let money_ratio = positive_flow / negative_flow; + result[i] = 100.0 - (100.0 / (1.0 + money_ratio)); + } + } + + Ok(result) +} + +/// Accumulation/Distribution Line. +/// +/// # Arguments +/// * `high` - High prices +/// * `low` - Low prices +/// * `close` - Close prices +/// * `volume` - Volume data +/// +/// # Returns +/// Vector of A/D line values +pub fn ad_line(high: &[f64], low: &[f64], close: &[f64], volume: &[f64]) -> Result> { + let n = close.len(); + if n != high.len() || n != low.len() || n != volume.len() { + return Err(RaptorError::length_mismatch(n, high.len())); + } + + if n == 0 { + return Ok(vec![]); + } + + let mut result = vec![0.0; n]; + + for i in 0..n { + let hl_range = high[i] - low[i]; + + // Money Flow Multiplier + let mfm = if hl_range > 0.0 { + ((close[i] - low[i]) - (high[i] - close[i])) / hl_range + } else { + 0.0 + }; + + // Money Flow Volume + let mfv = mfm * volume[i]; + + // Accumulate + if i == 0 { + result[i] = mfv; + } else { + result[i] = result[i - 1] + mfv; + } + } + + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_vwap() { + let high = vec![52.0, 53.0, 54.0, 53.0, 52.0]; + let low = vec![50.0, 51.0, 52.0, 51.0, 50.0]; + let close = vec![51.0, 52.0, 53.0, 52.0, 51.0]; + let volume = vec![1000.0, 1500.0, 2000.0, 1500.0, 1000.0]; + + let result = vwap(&high, &low, &close, &volume).unwrap(); + + // VWAP should be valid for all bars + assert!(!result[0].is_nan()); + assert!(!result[4].is_nan()); + + // VWAP should be between low and high range + assert!(result[4] >= 50.0 && result[4] <= 54.0); + } + + #[test] + fn test_obv() { + let close = vec![50.0, 51.0, 50.5, 52.0, 51.0]; + let volume = vec![1000.0, 1500.0, 1200.0, 1800.0, 1300.0]; + + let result = obv(&close, &volume).unwrap(); + + // OBV starts with first volume + assert!((result[0] - 1000.0).abs() < 1e-10); + + // Price up -> add volume + assert!((result[1] - 2500.0).abs() < 1e-10); + + // Price down -> subtract volume + assert!((result[2] - 1300.0).abs() < 1e-10); + } + + #[test] + fn test_mfi() { + let high = vec![ + 52.0, 53.0, 54.0, 53.0, 52.0, 53.0, 54.0, 55.0, 54.0, 53.0, 52.0, 53.0, 54.0, 55.0, + 56.0, + ]; + let low = vec![ + 50.0, 51.0, 52.0, 51.0, 50.0, 51.0, 52.0, 53.0, 52.0, 51.0, 50.0, 51.0, 52.0, 53.0, + 54.0, + ]; + let close = vec![ + 51.0, 52.0, 53.0, 52.0, 51.0, 52.0, 53.0, 54.0, 53.0, 52.0, 51.0, 52.0, 53.0, 54.0, + 55.0, + ]; + let volume = vec![1000.0; 15]; + + let result = mfi(&high, &low, &close, &volume, 14).unwrap(); + + // MFI should be valid from index 14 + assert!(result[13].is_nan()); + assert!(!result[14].is_nan()); + assert!(result[14] >= 0.0 && result[14] <= 100.0); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9490344 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,58 @@ +// Suppress warning from PyO3 macro expansion (fixed in newer PyO3 versions) +#![allow(non_local_definitions)] + +//! RaptorBT - High-performance Rust backtesting engine for quant5. +//! +//! This crate provides a complete backtesting solution with: +//! - Technical indicators (SMA, EMA, RSI, MACD, etc.) +//! - Portfolio simulation engine +//! - Multiple strategy types (single, basket, options, pairs, multi) +//! - Stop-loss and take-profit mechanisms +//! - Streaming metrics calculation + +use pyo3::prelude::*; + +pub mod core; +pub mod execution; +pub mod indicators; +pub mod metrics; +pub mod portfolio; +pub mod python; +pub mod signals; +pub mod stops; +pub mod strategies; + +/// Python module entry point +#[pymodule] +fn _raptorbt(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + // Register config classes + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + + // Register result classes + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + + // Register backtest functions + m.add_function(wrap_pyfunction!(python::bindings::run_single_backtest, m)?)?; + m.add_function(wrap_pyfunction!(python::bindings::run_basket_backtest, m)?)?; + m.add_function(wrap_pyfunction!(python::bindings::run_options_backtest, m)?)?; + m.add_function(wrap_pyfunction!(python::bindings::run_pairs_backtest, m)?)?; + m.add_function(wrap_pyfunction!(python::bindings::run_multi_backtest, m)?)?; + + // Register indicator functions + m.add_function(wrap_pyfunction!(python::bindings::sma, m)?)?; + m.add_function(wrap_pyfunction!(python::bindings::ema, m)?)?; + m.add_function(wrap_pyfunction!(python::bindings::rsi, m)?)?; + m.add_function(wrap_pyfunction!(python::bindings::macd, m)?)?; + m.add_function(wrap_pyfunction!(python::bindings::stochastic, m)?)?; + m.add_function(wrap_pyfunction!(python::bindings::atr, m)?)?; + m.add_function(wrap_pyfunction!(python::bindings::bollinger_bands, m)?)?; + m.add_function(wrap_pyfunction!(python::bindings::adx, m)?)?; + m.add_function(wrap_pyfunction!(python::bindings::vwap, m)?)?; + m.add_function(wrap_pyfunction!(python::bindings::supertrend, m)?)?; + + Ok(()) +} diff --git a/src/metrics/drawdown.rs b/src/metrics/drawdown.rs new file mode 100644 index 0000000..ddd952d --- /dev/null +++ b/src/metrics/drawdown.rs @@ -0,0 +1,348 @@ +//! Incremental drawdown tracking. + +/// Drawdown tracker for incremental portfolio value updates. +#[derive(Debug, Clone)] +pub struct DrawdownTracker { + /// Current peak value. + peak: f64, + /// Current drawdown value. + current_drawdown: f64, + /// Maximum drawdown seen. + max_drawdown: f64, + /// Current drawdown duration (bars since peak). + current_duration: usize, + /// Maximum drawdown duration. + max_duration: usize, + /// Value at drawdown start. + drawdown_start_value: f64, + /// Index at drawdown start. + drawdown_start_idx: usize, + /// Index at max drawdown. + max_drawdown_idx: usize, + /// Total count of updates. + count: usize, +} + +impl Default for DrawdownTracker { + fn default() -> Self { + Self::new() + } +} + +impl DrawdownTracker { + /// Create a new drawdown tracker. + pub fn new() -> Self { + Self { + peak: 0.0, + current_drawdown: 0.0, + max_drawdown: 0.0, + current_duration: 0, + max_duration: 0, + drawdown_start_value: 0.0, + drawdown_start_idx: 0, + max_drawdown_idx: 0, + count: 0, + } + } + + /// Create with initial value. + pub fn with_initial(initial_value: f64) -> Self { + Self { + peak: initial_value, + current_drawdown: 0.0, + max_drawdown: 0.0, + current_duration: 0, + max_duration: 0, + drawdown_start_value: initial_value, + drawdown_start_idx: 0, + max_drawdown_idx: 0, + count: 1, + } + } + + /// Update with new portfolio value. + pub fn update(&mut self, value: f64) { + self.count += 1; + + if value > self.peak { + // New peak - reset drawdown + self.peak = value; + self.current_drawdown = 0.0; + self.current_duration = 0; + self.drawdown_start_value = value; + self.drawdown_start_idx = self.count - 1; + } else { + // In drawdown + self.current_drawdown = (self.peak - value) / self.peak; + self.current_duration += 1; + + if self.current_drawdown > self.max_drawdown { + self.max_drawdown = self.current_drawdown; + self.max_drawdown_idx = self.count - 1; + } + + if self.current_duration > self.max_duration { + self.max_duration = self.current_duration; + } + } + } + + /// Get current drawdown as percentage. + #[inline] + pub fn current_drawdown_pct(&self) -> f64 { + self.current_drawdown * 100.0 + } + + /// Get maximum drawdown as percentage. + #[inline] + pub fn max_drawdown_pct(&self) -> f64 { + self.max_drawdown * 100.0 + } + + /// Get maximum drawdown as fraction. + #[inline] + pub fn max_drawdown(&self) -> f64 { + self.max_drawdown + } + + /// Get current peak value. + #[inline] + pub fn peak(&self) -> f64 { + self.peak + } + + /// Get current drawdown duration. + #[inline] + pub fn current_duration(&self) -> usize { + self.current_duration + } + + /// Get maximum drawdown duration. + #[inline] + pub fn max_duration(&self) -> usize { + self.max_duration + } + + /// Check if currently in drawdown. + #[inline] + pub fn in_drawdown(&self) -> bool { + self.current_drawdown > 0.0 + } + + /// Get index where max drawdown occurred. + #[inline] + pub fn max_drawdown_idx(&self) -> usize { + self.max_drawdown_idx + } + + /// Reset the tracker. + pub fn reset(&mut self) { + *self = Self::new(); + } +} + +/// Calculate drawdown curve from equity curve. +/// +/// # Arguments +/// * `equity_curve` - Portfolio values over time +/// +/// # Returns +/// Drawdown percentages at each point +pub fn calculate_drawdown_curve(equity_curve: &[f64]) -> Vec { + let n = equity_curve.len(); + if n == 0 { + return vec![]; + } + + let mut drawdown_curve = vec![0.0; n]; + let mut peak = equity_curve[0]; + + for i in 0..n { + if equity_curve[i] > peak { + peak = equity_curve[i]; + } + if peak > 0.0 { + drawdown_curve[i] = (peak - equity_curve[i]) / peak * 100.0; + } + } + + drawdown_curve +} + +/// Calculate maximum drawdown from equity curve. +/// +/// # Arguments +/// * `equity_curve` - Portfolio values over time +/// +/// # Returns +/// Maximum drawdown as percentage +pub fn max_drawdown(equity_curve: &[f64]) -> f64 { + let dd = calculate_drawdown_curve(equity_curve); + dd.iter().fold(0.0f64, |a, &b| a.max(b)) +} + +/// Calculate average drawdown from equity curve. +/// +/// # Arguments +/// * `equity_curve` - Portfolio values over time +/// +/// # Returns +/// Average drawdown as percentage +pub fn avg_drawdown(equity_curve: &[f64]) -> f64 { + let dd = calculate_drawdown_curve(equity_curve); + if dd.is_empty() { + return 0.0; + } + dd.iter().sum::() / dd.len() as f64 +} + +/// Find drawdown periods. +/// +/// # Arguments +/// * `equity_curve` - Portfolio values over time +/// +/// # Returns +/// Vector of (start_idx, end_idx, max_drawdown) tuples for each drawdown period +pub fn drawdown_periods(equity_curve: &[f64]) -> Vec<(usize, usize, f64)> { + let n = equity_curve.len(); + if n < 2 { + return vec![]; + } + + let mut periods = Vec::new(); + let mut peak = equity_curve[0]; + let mut peak_idx = 0; + let mut in_dd = false; + let mut dd_start = 0; + let mut max_dd = 0.0; + + for i in 1..n { + if equity_curve[i] > peak { + if in_dd { + // End of drawdown period + periods.push((dd_start, i - 1, max_dd)); + in_dd = false; + max_dd = 0.0; + } + peak = equity_curve[i]; + peak_idx = i; + } else if peak > 0.0 { + let dd = (peak - equity_curve[i]) / peak * 100.0; + if !in_dd && dd > 0.0 { + in_dd = true; + dd_start = peak_idx; + } + if dd > max_dd { + max_dd = dd; + } + } + } + + // Handle ongoing drawdown at end + if in_dd { + periods.push((dd_start, n - 1, max_dd)); + } + + periods +} + +/// Calculate Calmar ratio. +/// +/// # Arguments +/// * `total_return` - Total return as percentage +/// * `max_drawdown` - Maximum drawdown as percentage +/// +/// # Returns +/// Calmar ratio +pub fn calmar_ratio(total_return: f64, max_drawdown: f64) -> f64 { + if max_drawdown <= 0.0 { + return if total_return > 0.0 { + f64::INFINITY + } else { + 0.0 + }; + } + total_return / max_drawdown +} + +/// Calculate Ulcer Index (root mean square of drawdowns). +/// +/// # Arguments +/// * `equity_curve` - Portfolio values over time +/// +/// # Returns +/// Ulcer Index +pub fn ulcer_index(equity_curve: &[f64]) -> f64 { + let dd = calculate_drawdown_curve(equity_curve); + if dd.is_empty() { + return 0.0; + } + let sum_sq: f64 = dd.iter().map(|d| d * d).sum(); + (sum_sq / dd.len() as f64).sqrt() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_tracking() { + let mut tracker = DrawdownTracker::new(); + + tracker.update(100.0); + tracker.update(110.0); + tracker.update(105.0); // 4.5% drawdown + tracker.update(120.0); + tracker.update(100.0); // 16.67% drawdown + + assert!((tracker.max_drawdown_pct() - 16.67).abs() < 0.1); + assert!((tracker.peak() - 120.0).abs() < 1e-10); + } + + #[test] + fn test_drawdown_curve() { + let equity = vec![100.0, 110.0, 105.0, 120.0, 100.0]; + let dd = calculate_drawdown_curve(&equity); + + assert_eq!(dd.len(), 5); + assert!((dd[0] - 0.0).abs() < 1e-10); + assert!((dd[1] - 0.0).abs() < 1e-10); + assert!((dd[2] - 4.545).abs() < 0.1); // (110-105)/110 * 100 + assert!((dd[3] - 0.0).abs() < 1e-10); + assert!((dd[4] - 16.67).abs() < 0.1); // (120-100)/120 * 100 + } + + #[test] + fn test_max_drawdown() { + let equity = vec![100.0, 120.0, 90.0, 110.0, 85.0]; + let max_dd = max_drawdown(&equity); + + // Max DD should be (120-85)/120 = 29.17% + assert!((max_dd - 29.17).abs() < 0.1); + } + + #[test] + fn test_drawdown_periods() { + let equity = vec![100.0, 110.0, 105.0, 115.0, 100.0, 120.0]; + let periods = drawdown_periods(&equity); + + // Should have 2 drawdown periods + assert_eq!(periods.len(), 2); + } + + #[test] + fn test_calmar_ratio() { + // 50% return with 10% max drawdown + let calmar = calmar_ratio(50.0, 10.0); + assert!((calmar - 5.0).abs() < 1e-10); + } + + #[test] + fn test_ulcer_index() { + let equity = vec![100.0, 95.0, 90.0, 95.0, 100.0]; + let ui = ulcer_index(&equity); + + // Should be positive (there were drawdowns) + assert!(ui > 0.0); + } +} diff --git a/src/metrics/mod.rs b/src/metrics/mod.rs new file mode 100644 index 0000000..a45da02 --- /dev/null +++ b/src/metrics/mod.rs @@ -0,0 +1,9 @@ +//! Performance metrics for RaptorBT. + +pub mod drawdown; +pub mod streaming; +pub mod trade_stats; + +pub use drawdown::DrawdownTracker; +pub use streaming::StreamingMetrics; +pub use trade_stats::TradeStatistics; diff --git a/src/metrics/streaming.rs b/src/metrics/streaming.rs new file mode 100644 index 0000000..34c2a54 --- /dev/null +++ b/src/metrics/streaming.rs @@ -0,0 +1,404 @@ +//! Streaming metrics calculation using Welford's algorithm. +//! +//! Enables single-pass calculation of mean, variance, Sharpe ratio, and Sortino ratio. + +/// Streaming metrics calculator using Welford's algorithm. +/// +/// Allows incremental calculation of statistics without storing all values. +#[derive(Debug, Clone)] +pub struct StreamingMetrics { + /// Number of observations. + count: usize, + /// Running mean. + mean: f64, + /// Running M2 for variance calculation. + m2: f64, + /// Running M2 for downside variance (Sortino). + m2_downside: f64, + /// Target return for Sortino (default: 0). + target_return: f64, + /// Sum of returns (for total return calculation). + sum: f64, + /// Sum of positive returns. + sum_positive: f64, + /// Sum of negative returns. + sum_negative: f64, + /// Count of positive returns. + count_positive: usize, + /// Count of negative returns. + count_negative: usize, +} + +impl Default for StreamingMetrics { + fn default() -> Self { + Self::new() + } +} + +impl StreamingMetrics { + /// Create a new streaming metrics calculator. + pub fn new() -> Self { + Self { + count: 0, + mean: 0.0, + m2: 0.0, + m2_downside: 0.0, + target_return: 0.0, + sum: 0.0, + sum_positive: 0.0, + sum_negative: 0.0, + count_positive: 0, + count_negative: 0, + } + } + + /// Create with a custom target return for Sortino calculation. + pub fn with_target_return(mut self, target: f64) -> Self { + self.target_return = target; + self + } + + /// Update metrics with a new return value. + /// + /// Uses Welford's online algorithm for numerically stable variance calculation. + pub fn update(&mut self, return_value: f64) { + self.count += 1; + self.sum += return_value; + + // Track positive/negative + if return_value > 0.0 { + self.sum_positive += return_value; + self.count_positive += 1; + } else if return_value < 0.0 { + self.sum_negative += return_value; + self.count_negative += 1; + } + + // Welford's algorithm for mean and variance + let delta = return_value - self.mean; + self.mean += delta / self.count as f64; + let delta2 = return_value - self.mean; + self.m2 += delta * delta2; + + // Downside variance (for Sortino) + let downside = (return_value - self.target_return).min(0.0); + let _delta_down = downside - (self.m2_downside / self.count.max(1) as f64).sqrt(); + self.m2_downside += downside * downside; + } + + /// Get the number of observations. + #[inline] + pub fn count(&self) -> usize { + self.count + } + + /// Get the running mean. + #[inline] + pub fn mean(&self) -> f64 { + self.mean + } + + /// Get the sample variance. + pub fn variance(&self) -> f64 { + if self.count < 2 { + return 0.0; + } + self.m2 / (self.count - 1) as f64 + } + + /// Get the population variance. + pub fn variance_population(&self) -> f64 { + if self.count == 0 { + return 0.0; + } + self.m2 / self.count as f64 + } + + /// Get the sample standard deviation. + pub fn std_dev(&self) -> f64 { + self.variance().sqrt() + } + + /// Get the downside standard deviation (for Sortino). + pub fn downside_std_dev(&self) -> f64 { + if self.count < 2 { + return 0.0; + } + (self.m2_downside / (self.count - 1) as f64).sqrt() + } + + /// Calculate Sharpe ratio. + /// + /// # Arguments + /// * `periods_per_year` - Number of periods per year (e.g., 252 for daily) + /// * `risk_free_rate` - Annual risk-free rate (default: 0) + /// + /// # Returns + /// Annualized Sharpe ratio + pub fn sharpe_ratio(&self, periods_per_year: f64) -> f64 { + self.sharpe_ratio_with_rf(periods_per_year, 0.0) + } + + /// Calculate Sharpe ratio with custom risk-free rate. + pub fn sharpe_ratio_with_rf(&self, periods_per_year: f64, risk_free_rate: f64) -> f64 { + let std = self.std_dev(); + if std == 0.0 || self.count < 2 { + return 0.0; + } + + let rf_per_period = risk_free_rate / periods_per_year; + let excess_return = self.mean - rf_per_period; + let annualized_excess = excess_return * periods_per_year; + let annualized_std = std * periods_per_year.sqrt(); + + annualized_excess / annualized_std + } + + /// Calculate Sortino ratio. + /// + /// # Arguments + /// * `periods_per_year` - Number of periods per year (e.g., 252 for daily) + /// + /// # Returns + /// Annualized Sortino ratio + pub fn sortino_ratio(&self, periods_per_year: f64) -> f64 { + let downside_std = self.downside_std_dev(); + if downside_std == 0.0 || self.count < 2 { + return if self.mean > 0.0 { f64::INFINITY } else { 0.0 }; + } + + let excess_return = self.mean - self.target_return; + let annualized_excess = excess_return * periods_per_year; + let annualized_downside_std = downside_std * periods_per_year.sqrt(); + + annualized_excess / annualized_downside_std + } + + /// Get total return. + pub fn total_return(&self) -> f64 { + self.sum + } + + /// Get average positive return. + pub fn avg_positive_return(&self) -> f64 { + if self.count_positive == 0 { + return 0.0; + } + self.sum_positive / self.count_positive as f64 + } + + /// Get average negative return. + pub fn avg_negative_return(&self) -> f64 { + if self.count_negative == 0 { + return 0.0; + } + self.sum_negative / self.count_negative as f64 + } + + /// Get win rate (fraction of positive returns). + pub fn win_rate(&self) -> f64 { + if self.count == 0 { + return 0.0; + } + self.count_positive as f64 / self.count as f64 + } + + /// Get profit factor (sum of profits / sum of losses). + pub fn profit_factor(&self) -> f64 { + if self.sum_negative == 0.0 { + return if self.sum_positive > 0.0 { + f64::INFINITY + } else { + 0.0 + }; + } + self.sum_positive / self.sum_negative.abs() + } + + /// Get omega ratio (same as profit factor for return-based calculation). + /// Omega = (sum of returns above threshold) / |sum of returns below threshold| + /// With threshold = 0, this equals profit_factor. + pub fn omega_ratio(&self) -> f64 { + self.profit_factor() + } + + /// Reset all metrics. + pub fn reset(&mut self) { + *self = Self::new(); + } + + /// Merge two streaming metrics (for parallel computation). + pub fn merge(&mut self, other: &StreamingMetrics) { + if other.count == 0 { + return; + } + if self.count == 0 { + *self = other.clone(); + return; + } + + let combined_count = self.count + other.count; + let delta = other.mean - self.mean; + + // Merge means + let combined_mean = self.mean + delta * other.count as f64 / combined_count as f64; + + // Merge M2 (parallel variance) + let combined_m2 = self.m2 + + other.m2 + + delta * delta * self.count as f64 * other.count as f64 / combined_count as f64; + + // Update state + self.count = combined_count; + self.mean = combined_mean; + self.m2 = combined_m2; + self.sum += other.sum; + self.sum_positive += other.sum_positive; + self.sum_negative += other.sum_negative; + self.count_positive += other.count_positive; + self.count_negative += other.count_negative; + self.m2_downside += other.m2_downside; // Approximation + } +} + +/// Calculate Sharpe ratio from a slice of returns. +pub fn sharpe_ratio(returns: &[f64], periods_per_year: f64, risk_free_rate: f64) -> f64 { + let mut metrics = StreamingMetrics::new(); + for &r in returns { + if !r.is_nan() { + metrics.update(r); + } + } + metrics.sharpe_ratio_with_rf(periods_per_year, risk_free_rate) +} + +/// Calculate Sortino ratio from a slice of returns. +pub fn sortino_ratio(returns: &[f64], periods_per_year: f64) -> f64 { + let mut metrics = StreamingMetrics::new(); + for &r in returns { + if !r.is_nan() { + metrics.update(r); + } + } + metrics.sortino_ratio(periods_per_year) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_statistics() { + let mut metrics = StreamingMetrics::new(); + let values = vec![1.0, 2.0, 3.0, 4.0, 5.0]; + + for v in &values { + metrics.update(*v); + } + + assert_eq!(metrics.count(), 5); + assert!((metrics.mean() - 3.0).abs() < 1e-10); + + // Sample variance of [1,2,3,4,5] = 2.5 + assert!((metrics.variance() - 2.5).abs() < 1e-10); + } + + #[test] + fn test_welford_numerical_stability() { + let mut metrics = StreamingMetrics::new(); + + // Large values that might cause numerical issues with naive algorithm + let base = 1e10; + let values = vec![base + 1.0, base + 2.0, base + 3.0]; + + for v in &values { + metrics.update(*v); + } + + // Mean should be base + 2 + assert!((metrics.mean() - (base + 2.0)).abs() < 1e-5); + + // Variance should be 1.0 (same as [1, 2, 3]) + assert!((metrics.variance() - 1.0).abs() < 1e-5); + } + + #[test] + fn test_sharpe_ratio() { + let mut metrics = StreamingMetrics::new(); + + // Daily returns: 1%, 2%, -1%, 1.5%, 0.5% + let returns = vec![0.01, 0.02, -0.01, 0.015, 0.005]; + + for r in &returns { + metrics.update(*r); + } + + // Should produce a positive Sharpe ratio + let sharpe = metrics.sharpe_ratio(252.0); + assert!(sharpe > 0.0); + } + + #[test] + fn test_sortino_ratio() { + let mut metrics = StreamingMetrics::new(); + + // Mix of positive and negative returns + let returns = vec![0.02, -0.01, 0.03, -0.02, 0.01]; + + for r in &returns { + metrics.update(*r); + } + + // Sortino should be different from Sharpe + let sharpe = metrics.sharpe_ratio(252.0); + let sortino = metrics.sortino_ratio(252.0); + + // With negative returns, Sortino penalizes only downside + assert!(sortino != sharpe); + } + + #[test] + fn test_win_rate_and_profit_factor() { + let mut metrics = StreamingMetrics::new(); + + // 3 wins, 2 losses + let returns = vec![0.02, -0.01, 0.03, -0.02, 0.01]; + + for r in &returns { + metrics.update(*r); + } + + // Win rate should be 60% + assert!((metrics.win_rate() - 0.6).abs() < 1e-10); + + // Profit factor = 0.06 / 0.03 = 2.0 + assert!((metrics.profit_factor() - 2.0).abs() < 1e-10); + } + + #[test] + fn test_merge() { + let mut m1 = StreamingMetrics::new(); + let mut m2 = StreamingMetrics::new(); + + // Split data between two calculators + for v in &[1.0, 2.0, 3.0] { + m1.update(*v); + } + for v in &[4.0, 5.0] { + m2.update(*v); + } + + // Merge + m1.merge(&m2); + + // Should match single calculator with all data + let mut combined = StreamingMetrics::new(); + for v in &[1.0, 2.0, 3.0, 4.0, 5.0] { + combined.update(*v); + } + + assert_eq!(m1.count(), combined.count()); + assert!((m1.mean() - combined.mean()).abs() < 1e-10); + assert!((m1.variance() - combined.variance()).abs() < 1e-10); + } +} diff --git a/src/metrics/trade_stats.rs b/src/metrics/trade_stats.rs new file mode 100644 index 0000000..a20a144 --- /dev/null +++ b/src/metrics/trade_stats.rs @@ -0,0 +1,361 @@ +//! Trade statistics calculation. + +use crate::core::types::Trade; + +/// Comprehensive trade statistics. +#[derive(Debug, Clone, Default)] +pub struct TradeStatistics { + /// Total number of trades. + pub total_trades: usize, + /// Number of winning trades. + pub winning_trades: usize, + /// Number of losing trades. + pub losing_trades: usize, + /// Number of breakeven trades. + pub breakeven_trades: usize, + /// Win rate (as percentage). + pub win_rate: f64, + /// Average win amount. + pub avg_win: f64, + /// Average loss amount. + pub avg_loss: f64, + /// Largest win. + pub largest_win: f64, + /// Largest loss. + pub largest_loss: f64, + /// Total profit. + pub total_profit: f64, + /// Total loss. + pub total_loss: f64, + /// Net profit. + pub net_profit: f64, + /// Profit factor. + pub profit_factor: f64, + /// Expected value per trade. + pub expectancy: f64, + /// Average trade return percentage. + pub avg_return_pct: f64, + /// Average holding period (bars). + pub avg_holding_period: f64, + /// Max consecutive wins. + pub max_consecutive_wins: usize, + /// Max consecutive losses. + pub max_consecutive_losses: usize, + /// Average win/loss ratio. + pub avg_win_loss_ratio: f64, + /// Recovery factor (net profit / max loss). + pub recovery_factor: f64, + /// Payoff ratio (avg win / avg loss). + pub payoff_ratio: f64, +} + +impl TradeStatistics { + /// Calculate statistics from a list of trades. + pub fn from_trades(trades: &[Trade]) -> Self { + let mut stats = Self::default(); + + if trades.is_empty() { + return stats; + } + + stats.total_trades = trades.len(); + + // Categorize trades + for trade in trades { + if trade.pnl > 0.0 { + stats.winning_trades += 1; + stats.total_profit += trade.pnl; + if trade.pnl > stats.largest_win { + stats.largest_win = trade.pnl; + } + } else if trade.pnl < 0.0 { + stats.losing_trades += 1; + stats.total_loss += trade.pnl.abs(); + if trade.pnl.abs() > stats.largest_loss { + stats.largest_loss = trade.pnl.abs(); + } + } else { + stats.breakeven_trades += 1; + } + } + + // Calculate ratios + stats.net_profit = stats.total_profit - stats.total_loss; + + if stats.total_trades > 0 { + stats.win_rate = stats.winning_trades as f64 / stats.total_trades as f64 * 100.0; + } + + if stats.winning_trades > 0 { + stats.avg_win = stats.total_profit / stats.winning_trades as f64; + } + + if stats.losing_trades > 0 { + stats.avg_loss = stats.total_loss / stats.losing_trades as f64; + } + + if stats.total_loss > 0.0 { + stats.profit_factor = stats.total_profit / stats.total_loss; + } else if stats.total_profit > 0.0 { + stats.profit_factor = f64::INFINITY; + } + + if stats.avg_loss > 0.0 { + stats.payoff_ratio = stats.avg_win / stats.avg_loss; + } + + // Expectancy + if stats.total_trades > 0 { + stats.expectancy = stats.net_profit / stats.total_trades as f64; + } + + // Average return percentage + if stats.total_trades > 0 { + stats.avg_return_pct = + trades.iter().map(|t| t.return_pct).sum::() / stats.total_trades as f64; + } + + // Average holding period + if stats.total_trades > 0 { + stats.avg_holding_period = trades + .iter() + .map(|t| t.holding_period() as f64) + .sum::() + / stats.total_trades as f64; + } + + // Consecutive wins/losses + let (max_wins, max_losses) = calculate_consecutive(trades); + stats.max_consecutive_wins = max_wins; + stats.max_consecutive_losses = max_losses; + + // Recovery factor + if stats.largest_loss > 0.0 { + stats.recovery_factor = stats.net_profit / stats.largest_loss; + } + + // Win/loss ratio + if stats.losing_trades > 0 { + stats.avg_win_loss_ratio = stats.winning_trades as f64 / stats.losing_trades as f64; + } + + stats + } + + /// Get summary as formatted string. + pub fn summary(&self) -> String { + format!( + "Trades: {} | Win Rate: {:.1}% | Profit Factor: {:.2} | Net: {:.2}", + self.total_trades, self.win_rate, self.profit_factor, self.net_profit + ) + } + + /// Check if strategy is profitable. + pub fn is_profitable(&self) -> bool { + self.net_profit > 0.0 + } + + /// Get edge (expected value as percentage of average trade). + pub fn edge(&self) -> f64 { + if self.total_trades == 0 { + return 0.0; + } + let avg_trade = self.net_profit / self.total_trades as f64; + let avg_cost = (self.total_profit + self.total_loss) / self.total_trades as f64; + if avg_cost > 0.0 { + avg_trade / avg_cost * 100.0 + } else { + 0.0 + } + } +} + +/// Calculate maximum consecutive wins and losses. +fn calculate_consecutive(trades: &[Trade]) -> (usize, usize) { + let mut max_wins = 0; + let mut max_losses = 0; + let mut current_wins = 0; + let mut current_losses = 0; + + for trade in trades { + if trade.pnl > 0.0 { + current_wins += 1; + current_losses = 0; + max_wins = max_wins.max(current_wins); + } else if trade.pnl < 0.0 { + current_losses += 1; + current_wins = 0; + max_losses = max_losses.max(current_losses); + } + } + + (max_wins, max_losses) +} + +/// Monthly returns breakdown. +#[derive(Debug, Clone, Default)] +pub struct MonthlyReturns { + /// Year. + pub year: i32, + /// Month (1-12). + pub month: u8, + /// Return percentage. + pub return_pct: f64, + /// Number of trades. + pub trade_count: usize, +} + +/// Calculate trade statistics by exit reason. +pub fn stats_by_exit_reason( + trades: &[Trade], +) -> std::collections::HashMap { + use crate::core::types::ExitReason; + use std::collections::HashMap; + + let mut grouped: HashMap> = HashMap::new(); + + for trade in trades { + grouped.entry(trade.exit_reason).or_default().push(trade); + } + + grouped + .into_iter() + .map(|(reason, trade_refs)| { + let owned_trades: Vec = trade_refs.into_iter().cloned().collect(); + (reason, TradeStatistics::from_trades(&owned_trades)) + }) + .collect() +} + +/// Calculate statistics for long vs short trades. +pub fn stats_by_direction(trades: &[Trade]) -> (TradeStatistics, TradeStatistics) { + use crate::core::types::Direction; + + let long_trades: Vec = trades + .iter() + .filter(|t| t.direction == Direction::Long) + .cloned() + .collect(); + + let short_trades: Vec = trades + .iter() + .filter(|t| t.direction == Direction::Short) + .cloned() + .collect(); + + ( + TradeStatistics::from_trades(&long_trades), + TradeStatistics::from_trades(&short_trades), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::types::{Direction, ExitReason}; + + fn sample_trades() -> Vec { + vec![ + Trade { + id: 1, + symbol: "TEST".to_string(), + entry_idx: 0, + exit_idx: 5, + entry_price: 100.0, + exit_price: 110.0, + size: 10.0, + direction: Direction::Long, + pnl: 100.0, // Win + return_pct: 10.0, + entry_time: 0, + exit_time: 5, + fees: 0.0, + exit_reason: ExitReason::Signal, + }, + Trade { + id: 2, + symbol: "TEST".to_string(), + entry_idx: 10, + exit_idx: 15, + entry_price: 100.0, + exit_price: 95.0, + size: 10.0, + direction: Direction::Long, + pnl: -50.0, // Loss + return_pct: -5.0, + entry_time: 10, + exit_time: 15, + fees: 0.0, + exit_reason: ExitReason::StopLoss, + }, + Trade { + id: 3, + symbol: "TEST".to_string(), + entry_idx: 20, + exit_idx: 25, + entry_price: 100.0, + exit_price: 108.0, + size: 10.0, + direction: Direction::Long, + pnl: 80.0, // Win + return_pct: 8.0, + entry_time: 20, + exit_time: 25, + fees: 0.0, + exit_reason: ExitReason::TakeProfit, + }, + ] + } + + #[test] + fn test_basic_stats() { + let trades = sample_trades(); + let stats = TradeStatistics::from_trades(&trades); + + assert_eq!(stats.total_trades, 3); + assert_eq!(stats.winning_trades, 2); + assert_eq!(stats.losing_trades, 1); + assert!((stats.win_rate - 66.67).abs() < 0.1); + } + + #[test] + fn test_profit_calculations() { + let trades = sample_trades(); + let stats = TradeStatistics::from_trades(&trades); + + assert!((stats.total_profit - 180.0).abs() < 1e-10); + assert!((stats.total_loss - 50.0).abs() < 1e-10); + assert!((stats.net_profit - 130.0).abs() < 1e-10); + assert!((stats.profit_factor - 3.6).abs() < 0.1); + } + + #[test] + fn test_consecutive() { + let trades = sample_trades(); + let (max_wins, max_losses) = calculate_consecutive(&trades); + + // W, L, W -> max consecutive wins = 1, max consecutive losses = 1 + assert_eq!(max_wins, 1); + assert_eq!(max_losses, 1); + } + + #[test] + fn test_stats_by_exit_reason() { + let trades = sample_trades(); + let by_reason = stats_by_exit_reason(&trades); + + // Should have 3 different exit reasons + assert!(by_reason.contains_key(&ExitReason::Signal)); + assert!(by_reason.contains_key(&ExitReason::StopLoss)); + assert!(by_reason.contains_key(&ExitReason::TakeProfit)); + } + + #[test] + fn test_empty_trades() { + let stats = TradeStatistics::from_trades(&[]); + + assert_eq!(stats.total_trades, 0); + assert!((stats.win_rate - 0.0).abs() < 1e-10); + assert!((stats.profit_factor - 0.0).abs() < 1e-10); + } +} diff --git a/src/portfolio/allocation.rs b/src/portfolio/allocation.rs new file mode 100644 index 0000000..55bfeab --- /dev/null +++ b/src/portfolio/allocation.rs @@ -0,0 +1,345 @@ +//! Capital allocation strategies for portfolio management. + +/// Allocation strategy for distributing capital across instruments. +#[derive(Debug, Clone)] +pub enum AllocationStrategy { + /// Equal weight across all instruments. + EqualWeight, + /// Fixed weight for each instrument. + FixedWeight(Vec), + /// Volatility-based weighting (inverse volatility). + InverseVolatility, + /// Risk parity (equal risk contribution). + RiskParity, + /// Maximum weight per instrument. + MaxWeight(f64), + /// Custom weights. + Custom(Vec<(String, f64)>), +} + +impl Default for AllocationStrategy { + fn default() -> Self { + AllocationStrategy::EqualWeight + } +} + +/// Capital allocator for managing position sizing and capital distribution. +#[derive(Debug, Clone)] +pub struct CapitalAllocator { + /// Total capital. + pub total_capital: f64, + /// Available capital (not in positions). + pub available_capital: f64, + /// Allocation strategy. + pub strategy: AllocationStrategy, + /// Maximum position size as fraction of capital. + pub max_position_size: f64, + /// Minimum position size (absolute). + pub min_position_size: f64, + /// Reserve capital fraction (never allocate). + pub reserve_fraction: f64, +} + +impl CapitalAllocator { + /// Create a new capital allocator. + pub fn new(total_capital: f64) -> Self { + Self { + total_capital, + available_capital: total_capital, + strategy: AllocationStrategy::EqualWeight, + max_position_size: 1.0, + min_position_size: 0.0, + reserve_fraction: 0.0, + } + } + + /// Set allocation strategy. + pub fn with_strategy(mut self, strategy: AllocationStrategy) -> Self { + self.strategy = strategy; + self + } + + /// Set maximum position size. + pub fn with_max_position(mut self, max_fraction: f64) -> Self { + self.max_position_size = max_fraction.clamp(0.0, 1.0); + self + } + + /// Set reserve fraction. + pub fn with_reserve(mut self, reserve: f64) -> Self { + self.reserve_fraction = reserve.clamp(0.0, 1.0); + self + } + + /// Calculate position size for a single instrument. + /// + /// # Arguments + /// * `price` - Entry price + /// * `num_instruments` - Total number of instruments in portfolio + /// * `instrument_weight` - Optional custom weight for this instrument + /// + /// # Returns + /// Position size in shares/contracts + pub fn calculate_position_size( + &self, + price: f64, + num_instruments: usize, + instrument_weight: Option, + ) -> f64 { + if price <= 0.0 || num_instruments == 0 { + return 0.0; + } + + // Calculate allocatable capital + let allocatable = self.available_capital * (1.0 - self.reserve_fraction); + + // Calculate weight + let weight = match &self.strategy { + AllocationStrategy::EqualWeight => 1.0 / num_instruments as f64, + AllocationStrategy::FixedWeight(weights) => { + if weights.is_empty() { + 1.0 / num_instruments as f64 + } else { + weights[0].min(self.max_position_size) + } + } + AllocationStrategy::MaxWeight(max) => (*max).min(1.0 / num_instruments as f64), + _ => instrument_weight.unwrap_or(1.0 / num_instruments as f64), + }; + + // Calculate allocation + let allocation = allocatable * weight.min(self.max_position_size); + + // Convert to shares + let shares = allocation / price; + + // Apply minimum size constraint + if shares * price < self.min_position_size { + return 0.0; + } + + shares + } + + /// Calculate position sizes for multiple instruments. + /// + /// # Arguments + /// * `prices` - Entry prices for each instrument + /// * `weights` - Optional weights for each instrument + /// + /// # Returns + /// Position sizes for each instrument + pub fn calculate_portfolio_sizes(&self, prices: &[f64], weights: Option<&[f64]>) -> Vec { + let n = prices.len(); + if n == 0 { + return vec![]; + } + + let allocatable = self.available_capital * (1.0 - self.reserve_fraction); + + // Get weights + let instrument_weights: Vec = match &self.strategy { + AllocationStrategy::EqualWeight => vec![1.0 / n as f64; n], + AllocationStrategy::FixedWeight(w) => { + if w.len() == n { + w.clone() + } else { + vec![1.0 / n as f64; n] + } + } + AllocationStrategy::MaxWeight(max) => { + let equal = 1.0 / n as f64; + vec![equal.min(*max); n] + } + _ => weights + .map(|w| w.to_vec()) + .unwrap_or_else(|| vec![1.0 / n as f64; n]), + }; + + // Normalize weights + let total_weight: f64 = instrument_weights.iter().sum(); + let normalized_weights: Vec = if total_weight > 0.0 { + instrument_weights + .iter() + .map(|w| w / total_weight) + .collect() + } else { + vec![1.0 / n as f64; n] + }; + + // Calculate sizes + prices + .iter() + .zip(normalized_weights.iter()) + .map(|(&price, &weight)| { + if price <= 0.0 { + return 0.0; + } + let allocation = allocatable * weight.min(self.max_position_size); + let shares = allocation / price; + if shares * price < self.min_position_size { + 0.0 + } else { + shares + } + }) + .collect() + } + + /// Calculate volatility-adjusted position size. + /// + /// # Arguments + /// * `price` - Entry price + /// * `volatility` - Instrument volatility (e.g., ATR) + /// * `risk_per_trade` - Risk per trade as fraction of capital + /// + /// # Returns + /// Position size + pub fn calculate_volatility_sized( + &self, + price: f64, + volatility: f64, + risk_per_trade: f64, + ) -> f64 { + if price <= 0.0 || volatility <= 0.0 { + return 0.0; + } + + let risk_amount = self.available_capital * risk_per_trade; + let size = risk_amount / volatility; + + // Apply maximum constraint + let max_allocation = self.available_capital * self.max_position_size; + let max_shares = max_allocation / price; + + size.min(max_shares) + } + + /// Allocate capital to a position. + /// + /// # Arguments + /// * `amount` - Amount to allocate + /// + /// # Returns + /// True if allocation succeeded + pub fn allocate(&mut self, amount: f64) -> bool { + if amount > self.available_capital { + return false; + } + self.available_capital -= amount; + true + } + + /// Release capital from a closed position. + /// + /// # Arguments + /// * `amount` - Amount to release (including P&L) + pub fn release(&mut self, amount: f64) { + self.available_capital += amount; + } + + /// Update total capital (e.g., after deposit/withdrawal or daily mark-to-market). + pub fn update_capital(&mut self, new_capital: f64) { + let diff = new_capital - self.total_capital; + self.total_capital = new_capital; + self.available_capital += diff; + } + + /// Get current utilization rate. + pub fn utilization(&self) -> f64 { + if self.total_capital <= 0.0 { + return 0.0; + } + 1.0 - (self.available_capital / self.total_capital) + } + + /// Reset allocator to initial state. + pub fn reset(&mut self) { + self.available_capital = self.total_capital; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_equal_weight() { + let allocator = CapitalAllocator::new(100_000.0); + + // 4 instruments, equal weight = 25% each + let size = allocator.calculate_position_size(100.0, 4, None); + + // Expected: 100000 * 0.25 / 100 = 250 shares + assert!((size - 250.0).abs() < 1e-10); + } + + #[test] + fn test_max_position() { + let allocator = CapitalAllocator::new(100_000.0).with_max_position(0.1); + + // Even with 1 instrument, max is 10% + let size = allocator.calculate_position_size(100.0, 1, None); + + // Expected: 100000 * 0.1 / 100 = 100 shares + assert!((size - 100.0).abs() < 1e-10); + } + + #[test] + fn test_portfolio_sizes() { + let allocator = CapitalAllocator::new(100_000.0); + + let prices = vec![100.0, 50.0, 200.0]; + let sizes = allocator.calculate_portfolio_sizes(&prices, None); + + assert_eq!(sizes.len(), 3); + + // Equal weight, each gets 1/3 of capital + // Instrument 1: 33333 / 100 = 333.33 + // Instrument 2: 33333 / 50 = 666.66 + // Instrument 3: 33333 / 200 = 166.66 + assert!((sizes[0] - 333.33).abs() < 1.0); + assert!((sizes[1] - 666.66).abs() < 1.0); + assert!((sizes[2] - 166.66).abs() < 1.0); + } + + #[test] + fn test_allocate_release() { + let mut allocator = CapitalAllocator::new(100_000.0); + + // Allocate 30000 + assert!(allocator.allocate(30_000.0)); + assert!((allocator.available_capital - 70_000.0).abs() < 1e-10); + + // Try to allocate more than available + assert!(!allocator.allocate(80_000.0)); + + // Release with profit + allocator.release(35_000.0); + assert!((allocator.available_capital - 105_000.0).abs() < 1e-10); + } + + #[test] + fn test_utilization() { + let mut allocator = CapitalAllocator::new(100_000.0); + + assert!((allocator.utilization() - 0.0).abs() < 1e-10); + + allocator.allocate(50_000.0); + assert!((allocator.utilization() - 0.5).abs() < 1e-10); + } + + #[test] + fn test_volatility_sizing() { + let allocator = CapitalAllocator::new(100_000.0).with_max_position(0.2); + + // Risk 1% per trade with ATR of 2 + let size = allocator.calculate_volatility_sized(100.0, 2.0, 0.01); + + // Risk amount: 100000 * 0.01 = 1000 + // Size: 1000 / 2 = 500 shares + // Max: 100000 * 0.2 / 100 = 200 shares + // Should be capped at max + assert!((size - 200.0).abs() < 1e-10); + } +} diff --git a/src/portfolio/engine.rs b/src/portfolio/engine.rs new file mode 100644 index 0000000..86ab836 --- /dev/null +++ b/src/portfolio/engine.rs @@ -0,0 +1,869 @@ +//! Event-driven portfolio simulation engine. + +use crate::core::types::{ + BacktestConfig, BacktestMetrics, BacktestResult, CompiledSignals, Direction, ExitReason, + OhlcvData, Price, StopConfig, TargetConfig, Trade, +}; +use crate::execution::{FeeModel, FillPrice, SlippageModel}; +use crate::indicators::volatility::atr; +use crate::metrics::streaming::StreamingMetrics; +use crate::portfolio::position::PositionManager; +use crate::signals::processor::SignalProcessor; + +/// Portfolio simulation engine. +/// +/// Single-pass O(n) algorithm for simulating portfolio performance. +#[derive(Debug)] +pub struct PortfolioEngine { + /// Configuration. + pub config: BacktestConfig, + /// Fee model. + pub fee_model: FeeModel, + /// Slippage model. + pub slippage_model: SlippageModel, + /// Fill price model. + pub fill_price: FillPrice, + /// Signal processor. + pub signal_processor: SignalProcessor, +} + +impl Default for PortfolioEngine { + fn default() -> Self { + Self::new(BacktestConfig::default()) + } +} + +impl PortfolioEngine { + /// Create a new portfolio engine with the given configuration. + pub fn new(config: BacktestConfig) -> Self { + let fee_model = FeeModel::percentage(config.fees); + let fill_price = if config.upon_bar_close { + FillPrice::Close + } else { + FillPrice::Open + }; + + Self { + config, + fee_model, + slippage_model: SlippageModel::None, + fill_price, + signal_processor: SignalProcessor::new(), + } + } + + /// Set fee model. + pub fn with_fee_model(mut self, fee_model: FeeModel) -> Self { + self.fee_model = fee_model; + self + } + + /// Set slippage model. + pub fn with_slippage_model(mut self, slippage_model: SlippageModel) -> Self { + self.slippage_model = slippage_model; + self + } + + /// Run backtest on single instrument. + /// + /// # Arguments + /// * `ohlcv` - OHLCV data + /// * `signals` - Compiled trading signals + /// + /// # Returns + /// Backtest result + pub fn run_single(&self, ohlcv: &OhlcvData, signals: &CompiledSignals) -> BacktestResult { + let n = ohlcv.len(); + assert_eq!(n, signals.len(), "OHLCV and signals must have same length"); + + // Clean signals + let (entries, exits) = self + .signal_processor + .clean_signals(&signals.entries, &signals.exits); + + // Initialize state + let mut position = PositionManager::new(signals.symbol.clone()); + let mut cash = self.config.initial_capital; + let mut equity_curve = vec![cash; n]; + let mut drawdown_curve = vec![0.0; n]; + let mut returns = vec![0.0; n]; + let mut trades: Vec = Vec::new(); + let mut streaming = StreamingMetrics::new(); + let mut peak_equity = cash; + + // Pre-calculate ATR for ATR-based stops + let atr_values = if matches!(self.config.stop, StopConfig::Atr { .. }) + || matches!(self.config.target, TargetConfig::Atr { .. }) + { + let period = match self.config.stop { + StopConfig::Atr { period, .. } => period, + _ => match self.config.target { + TargetConfig::Atr { period, .. } => period, + _ => 14, + }, + }; + atr(&ohlcv.high, &ohlcv.low, &ohlcv.close, period).unwrap_or_else(|_| vec![0.0; n]) + } else { + vec![0.0; n] + }; + + // Main simulation loop + for i in 0..n { + let close = ohlcv.close[i]; + let high = ohlcv.high[i]; + let low = ohlcv.low[i]; + let timestamp = ohlcv.timestamps[i]; + + // Update position price tracking + position.update_price(high, low); + + // Check for exits first (stops and signals) + if position.is_in_position() { + let mut exit_reason: Option = None; + let mut exit_price = close; + + // Check stop-loss + if position.is_stop_hit(low, high) { + exit_reason = Some(ExitReason::StopLoss); + exit_price = position.position.stop_price.unwrap(); + + // Adjust for gap through stop + match position.position.direction { + Direction::Long => { + if ohlcv.open[i] < exit_price { + exit_price = ohlcv.open[i]; + } + } + Direction::Short => { + if ohlcv.open[i] > exit_price { + exit_price = ohlcv.open[i]; + } + } + } + } + + // Check take-profit + if exit_reason.is_none() && position.is_target_hit(low, high) { + exit_reason = Some(ExitReason::TakeProfit); + exit_price = position.position.target_price.unwrap(); + } + + // Check exit signal + if exit_reason.is_none() && exits[i] { + exit_reason = Some(ExitReason::Signal); + exit_price = self.get_fill_price(ohlcv, i, signals.direction, false); + } + + // Execute exit + if let Some(reason) = exit_reason { + // Apply slippage + exit_price = self.slippage_model.apply( + exit_price, + position.position.direction, + false, + Some(ohlcv.volume[i]), + ); + + // Calculate fees + let fees = self.fee_model.calculate( + exit_price, + position.position.size, + position.position.direction, + ); + + // Close position + if let Some(trade) = position.close_position( + i, + timestamp, + exit_price, + ohlcv.timestamps[position.position.entry_idx], + reason, + fees, + ) { + // Update cash + let exit_value = exit_price * trade.size; + cash += exit_value - fees; + + // Track return for this trade + streaming.update(trade.return_pct / 100.0); + + trades.push(trade); + } + } + + // Update trailing stop if position still open + if position.is_in_position() { + if let StopConfig::Trailing { percent } = self.config.stop { + position.update_trailing_stop(percent); + } + } + } + + // Check for entries + if !position.is_in_position() && entries[i] { + let entry_price = self.get_fill_price(ohlcv, i, signals.direction, true); + + // Apply slippage + let adjusted_price = self.slippage_model.apply( + entry_price, + signals.direction, + true, + Some(ohlcv.volume[i]), + ); + + // Calculate position size + // VectorBT formula: size = cash / (price * (1 + fees)) + // This ensures the position value plus entry fee equals available cash + let fee_rate = self.config.fees; + let size = if let Some(ref sizes) = signals.position_sizes { + sizes[i] * cash / (adjusted_price * (1.0 + fee_rate)) + } else { + cash / (adjusted_price * (1.0 + fee_rate)) + }; + + if size > 0.0 { + // Calculate entry fees + let entry_fees = + self.fee_model + .calculate(adjusted_price, size, signals.direction); + + // Calculate stop and target prices + let (stop_price, target_price) = self.calculate_stop_target( + adjusted_price, + signals.direction, + &atr_values, + i, + ); + + // Open position (passing entry_fees for trade PnL tracking) + position.open_position( + i, + timestamp, + adjusted_price, + size, + signals.direction, + stop_price, + target_price, + entry_fees, + ); + + // Deduct cost + cash -= adjusted_price * size + entry_fees; + } + } + + // Calculate equity + let position_value = if position.is_in_position() { + close * position.position.size + } else { + 0.0 + }; + let equity = cash + position_value; + equity_curve[i] = equity; + + // Calculate drawdown + if equity > peak_equity { + peak_equity = equity; + } + drawdown_curve[i] = (peak_equity - equity) / peak_equity * 100.0; + + // Calculate return + if i > 0 { + returns[i] = (equity - equity_curve[i - 1]) / equity_curve[i - 1]; + } + } + + // Mark any open position at end of data (no exit fees, matching VectorBT behavior) + if position.is_in_position() { + let last_idx = n - 1; + let exit_price = ohlcv.close[last_idx]; + // No exit fees for EndOfData - position is marked-to-market but not actually closed + // This matches VectorBT's behavior for "Open" trades + let exit_fees = 0.0; + + if let Some(trade) = position.close_position( + last_idx, + ohlcv.timestamps[last_idx], + exit_price, + ohlcv.timestamps[position.position.entry_idx], + ExitReason::EndOfData, + exit_fees, + ) { + streaming.update(trade.return_pct / 100.0); + trades.push(trade); + } + } + + // Calculate final metrics + let metrics = self.calculate_metrics( + &equity_curve, + &drawdown_curve, + &returns, + &trades, + &streaming, + ); + + BacktestResult::new(metrics, equity_curve, drawdown_curve, trades, returns) + } + + /// Get fill price based on model. + fn get_fill_price( + &self, + ohlcv: &OhlcvData, + idx: usize, + direction: Direction, + is_entry: bool, + ) -> Price { + self.fill_price.get_price_from_arrays( + ohlcv.open[idx], + ohlcv.high[idx], + ohlcv.low[idx], + ohlcv.close[idx], + direction, + is_entry, + ) + } + + /// Calculate stop and target prices. + fn calculate_stop_target( + &self, + entry_price: Price, + direction: Direction, + atr_values: &[f64], + idx: usize, + ) -> (Option, Option) { + let multiplier = direction.multiplier(); + + // Calculate stop price + let stop_price = match self.config.stop { + StopConfig::None => None, + StopConfig::Fixed { percent } => Some(entry_price * (1.0 - multiplier * percent)), + StopConfig::Atr { multiplier: m, .. } => { + let atr = atr_values.get(idx).copied().unwrap_or(0.0); + if atr > 0.0 { + Some(entry_price - multiplier * m * atr) + } else { + None + } + } + StopConfig::Trailing { percent } => Some(entry_price * (1.0 - multiplier * percent)), + }; + + // Calculate target price + let target_price = match self.config.target { + TargetConfig::None => None, + TargetConfig::Fixed { percent } => Some(entry_price * (1.0 + multiplier * percent)), + TargetConfig::Atr { multiplier: m, .. } => { + let atr = atr_values.get(idx).copied().unwrap_or(0.0); + if atr > 0.0 { + Some(entry_price + multiplier * m * atr) + } else { + None + } + } + TargetConfig::RiskReward { ratio } => { + if let Some(stop) = stop_price { + let risk = (entry_price - stop).abs(); + Some(entry_price + multiplier * risk * ratio) + } else { + None + } + } + }; + + (stop_price, target_price) + } + + /// Calculate backtest metrics. + fn calculate_metrics( + &self, + equity_curve: &[f64], + drawdown_curve: &[f64], + returns: &[f64], + trades: &[Trade], + _streaming: &StreamingMetrics, + ) -> BacktestMetrics { + let start_value = self.config.initial_capital; + let end_value = *equity_curve.last().unwrap_or(&start_value); + + let total_return_pct = (end_value - start_value) / start_value * 100.0; + let max_drawdown_pct = drawdown_curve.iter().fold(0.0f64, |a, &b| a.max(b)); + + // Calculate max drawdown duration + let max_drawdown_duration = self.calculate_max_drawdown_duration(drawdown_curve); + + // Trade statistics + let total_trades = trades.len(); + + // Separate closed vs open trades (EndOfData means still open) + let total_open_trades = trades + .iter() + .filter(|t| matches!(t.exit_reason, ExitReason::EndOfData)) + .count(); + let total_closed_trades = total_trades.saturating_sub(total_open_trades); + + // Open trade PnL + let open_trade_pnl: f64 = trades + .iter() + .filter(|t| matches!(t.exit_reason, ExitReason::EndOfData)) + .map(|t| t.pnl) + .sum(); + + // Only count closed trades for win/loss statistics + let closed_trades: Vec<_> = trades + .iter() + .filter(|t| !matches!(t.exit_reason, ExitReason::EndOfData)) + .collect(); + + let winning_trades = closed_trades.iter().filter(|t| t.pnl > 0.0).count(); + let losing_trades = closed_trades.iter().filter(|t| t.pnl < 0.0).count(); + + let win_rate_pct = if total_closed_trades > 0 { + winning_trades as f64 / total_closed_trades as f64 * 100.0 + } else { + 0.0 + }; + + // Total fees paid + let total_fees_paid: f64 = trades.iter().map(|t| t.fees).sum(); + + // Best and worst trade + let best_trade_pct = trades + .iter() + .map(|t| t.return_pct) + .fold(f64::NEG_INFINITY, |a, b| a.max(b)); + let best_trade_pct = if best_trade_pct.is_infinite() { + 0.0 + } else { + best_trade_pct + }; + + let worst_trade_pct = trades + .iter() + .map(|t| t.return_pct) + .fold(f64::INFINITY, |a, b| a.min(b)); + let worst_trade_pct = if worst_trade_pct.is_infinite() { + 0.0 + } else { + worst_trade_pct + }; + + // Profit factor (based on closed trades) + let gross_profit: f64 = closed_trades + .iter() + .filter(|t| t.pnl > 0.0) + .map(|t| t.pnl) + .sum(); + let gross_loss: f64 = closed_trades + .iter() + .filter(|t| t.pnl < 0.0) + .map(|t| t.pnl.abs()) + .sum(); + let profit_factor = if gross_loss > 0.0 { + gross_profit / gross_loss + } else if gross_profit > 0.0 { + f64::INFINITY + } else { + 0.0 + }; + + // Expectancy = average trade PnL + let expectancy = if total_closed_trades > 0 { + closed_trades.iter().map(|t| t.pnl).sum::() / total_closed_trades as f64 + } else { + 0.0 + }; + + // SQN = (Expectancy / StdDev of trade PnL) * sqrt(total trades) + let sqn = if total_closed_trades > 1 { + let trade_pnls: Vec = closed_trades.iter().map(|t| t.pnl).collect(); + let mean = expectancy; + let variance = trade_pnls.iter().map(|p| (p - mean).powi(2)).sum::() + / (total_closed_trades - 1) as f64; + let std_dev = variance.sqrt(); + if std_dev > 0.0 { + (mean / std_dev) * (total_closed_trades as f64).sqrt() + } else { + 0.0 + } + } else { + 0.0 + }; + + // Average returns + let avg_trade_return_pct = if total_trades > 0 { + trades.iter().map(|t| t.return_pct).sum::() / total_trades as f64 + } else { + 0.0 + }; + + let avg_win_pct = if winning_trades > 0 { + closed_trades + .iter() + .filter(|t| t.pnl > 0.0) + .map(|t| t.return_pct) + .sum::() + / winning_trades as f64 + } else { + 0.0 + }; + + let avg_loss_pct = if losing_trades > 0 { + closed_trades + .iter() + .filter(|t| t.pnl < 0.0) + .map(|t| t.return_pct) + .sum::() + / losing_trades as f64 + } else { + 0.0 + }; + + // Average winning/losing trade duration + let avg_winning_duration = if winning_trades > 0 { + closed_trades + .iter() + .filter(|t| t.pnl > 0.0) + .map(|t| t.holding_period() as f64) + .sum::() + / winning_trades as f64 + } else { + 0.0 + }; + + let avg_losing_duration = if losing_trades > 0 { + closed_trades + .iter() + .filter(|t| t.pnl < 0.0) + .map(|t| t.holding_period() as f64) + .sum::() + / losing_trades as f64 + } else { + 0.0 + }; + + // Consecutive wins/losses + let (max_consecutive_wins, max_consecutive_losses) = self.calculate_consecutive(trades); + + // Holding period + let avg_holding_period = if total_trades > 0 { + trades + .iter() + .map(|t| t.holding_period() as f64) + .sum::() + / total_trades as f64 + } else { + 0.0 + }; + + // Exposure (time in market) + let bars_in_position: usize = trades.iter().map(|t| t.holding_period()).sum(); + let exposure_pct = if !equity_curve.is_empty() { + bars_in_position as f64 / equity_curve.len() as f64 * 100.0 + } else { + 0.0 + }; + + // Risk-adjusted metrics (calculated from daily portfolio returns, not trade returns) + // This matches VectorBT's calculation methodology + let (sharpe_ratio, sortino_ratio, omega_ratio) = self.calculate_risk_metrics(returns); + + // Calmar ratio: CAGR / max drawdown + // VectorBT uses Compound Annual Growth Rate (CAGR) + let num_periods = equity_curve.len().max(1) as f64; + let years = num_periods / 365.25; // Convert to years using 365.25 days + let total_return_frac = total_return_pct / 100.0; + // CAGR = (end/start)^(1/years) - 1 = (1 + total_return)^(1/years) - 1 + let cagr = if years > 0.0 { + (1.0 + total_return_frac).powf(1.0 / years) - 1.0 + } else { + 0.0 + }; + let calmar_ratio = if max_drawdown_pct > 0.0 { + cagr / (max_drawdown_pct / 100.0) // Both as fractions + } else if total_return_pct > 0.0 { + f64::INFINITY + } else { + 0.0 + }; + + BacktestMetrics { + total_return_pct, + sharpe_ratio, + sortino_ratio, + calmar_ratio, + omega_ratio, + max_drawdown_pct, + max_drawdown_duration, + win_rate_pct, + profit_factor, + expectancy, + sqn, + total_trades, + total_closed_trades, + total_open_trades, + open_trade_pnl, + winning_trades, + losing_trades, + start_value, + end_value, + total_fees_paid, + best_trade_pct, + worst_trade_pct, + avg_trade_return_pct, + avg_win_pct, + avg_loss_pct, + avg_winning_duration, + avg_losing_duration, + max_consecutive_wins, + max_consecutive_losses, + avg_holding_period, + exposure_pct, + } + } + + /// Calculate max drawdown duration from drawdown curve. + fn calculate_max_drawdown_duration(&self, drawdown_curve: &[f64]) -> usize { + let mut max_duration = 0; + let mut current_duration = 0; + + for &dd in drawdown_curve { + if dd > 0.0 { + current_duration += 1; + max_duration = max_duration.max(current_duration); + } else { + current_duration = 0; + } + } + + max_duration + } + + /// Calculate max consecutive wins and losses. + fn calculate_consecutive(&self, trades: &[Trade]) -> (usize, usize) { + let mut max_wins = 0; + let mut max_losses = 0; + let mut current_wins = 0; + let mut current_losses = 0; + + for trade in trades { + if trade.pnl > 0.0 { + current_wins += 1; + current_losses = 0; + max_wins = max_wins.max(current_wins); + } else if trade.pnl < 0.0 { + current_losses += 1; + current_wins = 0; + max_losses = max_losses.max(current_losses); + } + } + + (max_wins, max_losses) + } + + /// Calculate risk-adjusted metrics from daily portfolio returns. + /// Returns (sharpe_ratio, sortino_ratio, omega_ratio). + /// Uses 365 days for annualization to match VectorBT. + fn calculate_risk_metrics(&self, returns: &[f64]) -> (f64, f64, f64) { + if returns.len() < 2 { + return (0.0, 0.0, 1.0); + } + + // VectorBT uses 365 days (calendar days) for annualization + let periods_per_year: f64 = 365.0; + let _n = returns.len() as f64; + + // Filter out NaN values + let valid_returns: Vec = returns.iter().filter(|r| !r.is_nan()).copied().collect(); + + if valid_returns.len() < 2 { + return (0.0, 0.0, 1.0); + } + + let n_valid = valid_returns.len() as f64; + + // Calculate mean return + let mean = valid_returns.iter().sum::() / n_valid; + + // Calculate standard deviation + let variance = valid_returns + .iter() + .map(|r| (r - mean).powi(2)) + .sum::() + / (n_valid - 1.0); + let std_dev = variance.sqrt(); + + // Sharpe Ratio = (mean * periods_per_year) / (std_dev * sqrt(periods_per_year)) + // Simplified: Sharpe = mean / std_dev * sqrt(periods_per_year) + let sharpe_ratio = if std_dev > 0.0 { + (mean / std_dev) * periods_per_year.sqrt() + } else { + 0.0 + }; + + // Sortino Ratio - uses downside deviation (only negative returns) + let downside_returns: Vec = valid_returns + .iter() + .filter(|&&r| r < 0.0) + .copied() + .collect(); + + let downside_variance = if !downside_returns.is_empty() { + downside_returns.iter().map(|r| r.powi(2)).sum::() / n_valid // Divide by total count, not downside count + } else { + 0.0 + }; + let downside_std = downside_variance.sqrt(); + + let sortino_ratio = if downside_std > 0.0 { + (mean / downside_std) * periods_per_year.sqrt() + } else if mean > 0.0 { + f64::INFINITY + } else { + 0.0 + }; + + // Omega Ratio = sum of returns above threshold / |sum of returns below threshold| + // With threshold = 0 + let sum_positive: f64 = valid_returns.iter().filter(|&&r| r > 0.0).sum(); + let sum_negative: f64 = valid_returns + .iter() + .filter(|&&r| r < 0.0) + .map(|r| r.abs()) + .sum(); + + let omega_ratio = if sum_negative > 0.0 { + sum_positive / sum_negative + } else if sum_positive > 0.0 { + f64::INFINITY + } else { + 1.0 + }; + + (sharpe_ratio, sortino_ratio, omega_ratio) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_ohlcv() -> OhlcvData { + OhlcvData { + timestamps: (0..20).map(|i| i as i64).collect(), + open: vec![ + 100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 104.0, 103.0, 102.0, 101.0, 100.0, 101.0, + 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 109.0, + ], + high: vec![ + 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 105.0, 104.0, 103.0, 102.0, 101.0, 102.0, + 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 109.0, 110.0, + ], + low: vec![ + 99.0, 100.0, 101.0, 102.0, 103.0, 104.0, 103.0, 102.0, 101.0, 100.0, 99.0, 100.0, + 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, + ], + close: vec![ + 100.5, 101.5, 102.5, 103.5, 104.5, 105.0, 104.0, 103.0, 102.0, 101.0, 100.5, 101.5, + 102.5, 103.5, 104.5, 105.5, 106.5, 107.5, 108.5, 109.5, + ], + volume: vec![1000.0; 20], + } + } + + fn sample_signals() -> CompiledSignals { + CompiledSignals { + symbol: "TEST".to_string(), + entries: vec![ + false, true, false, false, false, false, false, false, false, false, false, true, + false, false, false, false, false, false, false, false, + ], + exits: vec![ + false, false, false, false, false, true, false, false, false, false, false, false, + false, false, false, true, false, false, false, false, + ], + position_sizes: None, + direction: Direction::Long, + weight: 1.0, + } + } + + #[test] + fn test_basic_backtest() { + let config = BacktestConfig { + initial_capital: 100_000.0, + fees: 0.0, + slippage: 0.0, + stop: StopConfig::None, + target: TargetConfig::None, + upon_bar_close: true, + }; + + let engine = PortfolioEngine::new(config); + let ohlcv = sample_ohlcv(); + let signals = sample_signals(); + + let result = engine.run_single(&ohlcv, &signals); + + // Should have 2 trades + assert_eq!(result.trades.len(), 2); + + // First trade: entry at 101.5, exit at 105.0 + let trade1 = &result.trades[0]; + assert!((trade1.entry_price - 101.5).abs() < 1e-10); + assert!((trade1.exit_price - 105.0).abs() < 1e-10); + assert!(trade1.pnl > 0.0); // Profitable + + // Equity curve should have correct length + assert_eq!(result.equity_curve.len(), 20); + } + + #[test] + fn test_with_fees() { + let config = BacktestConfig { + initial_capital: 100_000.0, + fees: 0.001, // 0.1% + slippage: 0.0, + stop: StopConfig::None, + target: TargetConfig::None, + upon_bar_close: true, + }; + + let engine = PortfolioEngine::new(config); + let ohlcv = sample_ohlcv(); + let signals = sample_signals(); + + let result = engine.run_single(&ohlcv, &signals); + + // Trades should have fees deducted + for trade in &result.trades { + assert!(trade.fees > 0.0); + } + } + + #[test] + fn test_with_stop_loss() { + let config = BacktestConfig { + initial_capital: 100_000.0, + fees: 0.0, + slippage: 0.0, + stop: StopConfig::Fixed { percent: 0.02 }, // 2% stop + target: TargetConfig::None, + upon_bar_close: true, + }; + + let engine = PortfolioEngine::new(config); + + // Create data where stop would be hit + let mut ohlcv = sample_ohlcv(); + // Add a big drop after entry + ohlcv.low[3] = 95.0; // Big drop + ohlcv.close[3] = 96.0; + + let signals = sample_signals(); + let result = engine.run_single(&ohlcv, &signals); + + // First trade should exit on stop loss + assert_eq!(result.trades[0].exit_reason, ExitReason::StopLoss); + } +} diff --git a/src/portfolio/mod.rs b/src/portfolio/mod.rs new file mode 100644 index 0000000..8338c30 --- /dev/null +++ b/src/portfolio/mod.rs @@ -0,0 +1,9 @@ +//! Portfolio simulation engine for RaptorBT. + +pub mod allocation; +pub mod engine; +pub mod position; + +pub use allocation::{AllocationStrategy, CapitalAllocator}; +pub use engine::PortfolioEngine; +pub use position::PositionManager; diff --git a/src/portfolio/position.rs b/src/portfolio/position.rs new file mode 100644 index 0000000..c0b7cf1 --- /dev/null +++ b/src/portfolio/position.rs @@ -0,0 +1,366 @@ +//! Position tracking for portfolio management. + +use crate::core::types::{Direction, ExitReason, Position, Price, Timestamp, Trade}; + +/// Position manager for tracking open positions. +#[derive(Debug, Clone)] +pub struct PositionManager { + /// Current position state. + pub position: Position, + /// Trade counter for generating unique IDs. + trade_counter: u64, + /// Symbol being traded. + pub symbol: String, +} + +impl PositionManager { + /// Create a new position manager. + pub fn new(symbol: String) -> Self { + Self { + position: Position::new(), + trade_counter: 0, + symbol, + } + } + + /// Check if currently in a position. + #[inline] + pub fn is_in_position(&self) -> bool { + self.position.is_open + } + + /// Get current position direction. + pub fn current_direction(&self) -> Option { + if self.position.is_open { + Some(self.position.direction) + } else { + None + } + } + + /// Open a new position. + /// + /// # Arguments + /// * `idx` - Bar index + /// * `timestamp` - Entry timestamp + /// * `price` - Entry price + /// * `size` - Position size + /// * `direction` - Trade direction + /// * `stop_price` - Optional stop-loss price + /// * `target_price` - Optional take-profit price + /// * `entry_fees` - Entry fees (to track for PnL calculation) + /// + /// # Returns + /// True if position was opened, false if already in position + pub fn open_position( + &mut self, + idx: usize, + _timestamp: Timestamp, + price: Price, + size: f64, + direction: Direction, + stop_price: Option, + target_price: Option, + entry_fees: f64, + ) -> bool { + if self.position.is_open { + return false; + } + + self.position.open( + idx, + price, + size, + direction, + stop_price, + target_price, + entry_fees, + ); + true + } + + /// Close current position and generate a trade record. + /// + /// # Arguments + /// * `idx` - Bar index + /// * `timestamp` - Exit timestamp + /// * `price` - Exit price + /// * `entry_timestamp` - Entry timestamp (for trade record) + /// * `exit_reason` - Reason for exit + /// * `fees` - Transaction fees + /// + /// # Returns + /// Trade record if position was closed, None if no position + pub fn close_position( + &mut self, + idx: usize, + timestamp: Timestamp, + price: Price, + entry_timestamp: Timestamp, + exit_reason: ExitReason, + fees: f64, + ) -> Option { + if !self.position.is_open { + return None; + } + + let trade = self.create_trade(idx, timestamp, price, entry_timestamp, exit_reason, fees); + self.position.close(); + self.trade_counter += 1; + + Some(trade) + } + + /// Create a trade record from current position. + fn create_trade( + &self, + exit_idx: usize, + exit_timestamp: Timestamp, + exit_price: Price, + entry_timestamp: Timestamp, + exit_reason: ExitReason, + exit_fees: f64, + ) -> Trade { + let pos = &self.position; + let multiplier = pos.direction.multiplier(); + + // Calculate P&L (matching VectorBT: gross - entry_fees - exit_fees) + let gross_pnl = (exit_price - pos.entry_price) * pos.size * multiplier; + let total_fees = pos.entry_fees + exit_fees; + let pnl = gross_pnl - total_fees; + + // Calculate return percentage + let cost_basis = pos.entry_price * pos.size; + let return_pct = if cost_basis > 0.0 { + pnl / cost_basis * 100.0 + } else { + 0.0 + }; + + Trade { + id: self.trade_counter, + symbol: self.symbol.clone(), + entry_idx: pos.entry_idx, + exit_idx, + entry_price: pos.entry_price, + exit_price, + size: pos.size, + direction: pos.direction, + pnl, + return_pct, + entry_time: entry_timestamp, + exit_time: exit_timestamp, + fees: total_fees, + exit_reason, + } + } + + /// Update position with new price data (for trailing stops). + /// + /// # Arguments + /// * `high` - Current bar high + /// * `low` - Current bar low + pub fn update_price(&mut self, high: Price, low: Price) { + if self.position.is_open { + self.position.update_extremes(high, low); + } + } + + /// Calculate unrealized P&L at current price. + pub fn unrealized_pnl(&self, current_price: Price) -> f64 { + self.position.unrealized_pnl(current_price) + } + + /// Get current position value (market value of position). + pub fn position_value(&self, current_price: Price) -> f64 { + if !self.position.is_open { + return 0.0; + } + current_price * self.position.size + } + + /// Calculate position exposure (notional value as fraction of given capital). + pub fn exposure(&self, current_price: Price, capital: f64) -> f64 { + if capital <= 0.0 { + return 0.0; + } + self.position_value(current_price) / capital + } + + /// Check if stop-loss is hit. + pub fn is_stop_hit(&self, low: Price, high: Price) -> bool { + if !self.position.is_open { + return false; + } + + if let Some(stop) = self.position.stop_price { + match self.position.direction { + Direction::Long => low <= stop, + Direction::Short => high >= stop, + } + } else { + false + } + } + + /// Check if take-profit is hit. + pub fn is_target_hit(&self, low: Price, high: Price) -> bool { + if !self.position.is_open { + return false; + } + + if let Some(target) = self.position.target_price { + match self.position.direction { + Direction::Long => high >= target, + Direction::Short => low <= target, + } + } else { + false + } + } + + /// Update trailing stop. + /// + /// # Arguments + /// * `trail_percent` - Trailing stop percentage + pub fn update_trailing_stop(&mut self, trail_percent: f64) { + if !self.position.is_open { + return; + } + + match self.position.direction { + Direction::Long => { + // Trail below highest price since entry + let new_stop = self.position.highest_since_entry * (1.0 - trail_percent); + if let Some(current_stop) = self.position.stop_price { + if new_stop > current_stop { + self.position.stop_price = Some(new_stop); + } + } else { + self.position.stop_price = Some(new_stop); + } + } + Direction::Short => { + // Trail above lowest price since entry + let new_stop = self.position.lowest_since_entry * (1.0 + trail_percent); + if let Some(current_stop) = self.position.stop_price { + if new_stop < current_stop { + self.position.stop_price = Some(new_stop); + } + } else { + self.position.stop_price = Some(new_stop); + } + } + } + } + + /// Reset position manager for new backtest. + pub fn reset(&mut self) { + self.position = Position::new(); + self.trade_counter = 0; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_open_close_position() { + let mut pm = PositionManager::new("TEST".to_string()); + + // Open position + assert!(pm.open_position(0, 1000, 100.0, 10.0, Direction::Long, None, None)); + assert!(pm.is_in_position()); + + // Try to open another - should fail + assert!(!pm.open_position(1, 1001, 101.0, 10.0, Direction::Long, None, None)); + + // Close position with profit + let trade = pm + .close_position(5, 1005, 110.0, 1000, ExitReason::Signal, 2.0) + .unwrap(); + + assert!(!pm.is_in_position()); + assert_eq!(trade.entry_idx, 0); + assert_eq!(trade.exit_idx, 5); + assert!((trade.entry_price - 100.0).abs() < 1e-10); + assert!((trade.exit_price - 110.0).abs() < 1e-10); + + // P&L: (110 - 100) * 10 - 2 = 98 + assert!((trade.pnl - 98.0).abs() < 1e-10); + } + + #[test] + fn test_short_position() { + let mut pm = PositionManager::new("TEST".to_string()); + + pm.open_position(0, 1000, 100.0, 10.0, Direction::Short, None, None); + + // Close with profit (price went down) + let trade = pm + .close_position(5, 1005, 90.0, 1000, ExitReason::Signal, 2.0) + .unwrap(); + + // P&L: (100 - 90) * 10 * -(-1) - 2 = 98 + // For short: (entry - exit) * size = (100 - 90) * 10 = 100 gross, minus 2 fees = 98 + assert!((trade.pnl - 98.0).abs() < 1e-10); + } + + #[test] + fn test_stop_loss() { + let mut pm = PositionManager::new("TEST".to_string()); + + pm.open_position( + 0, + 1000, + 100.0, + 10.0, + Direction::Long, + Some(95.0), // Stop at 95 + None, + ); + + // Check stop not hit + assert!(!pm.is_stop_hit(96.0, 102.0)); + + // Check stop hit + assert!(pm.is_stop_hit(94.0, 102.0)); + } + + #[test] + fn test_trailing_stop() { + let mut pm = PositionManager::new("TEST".to_string()); + + pm.open_position(0, 1000, 100.0, 10.0, Direction::Long, None, None); + + // Update with higher price + pm.update_price(110.0, 98.0); + pm.update_trailing_stop(0.05); // 5% trail + + // Stop should be at 110 * 0.95 = 104.5 + assert!((pm.position.stop_price.unwrap() - 104.5).abs() < 1e-10); + + // Update with even higher price + pm.update_price(120.0, 108.0); + pm.update_trailing_stop(0.05); + + // Stop should move up to 120 * 0.95 = 114 + assert!((pm.position.stop_price.unwrap() - 114.0).abs() < 1e-10); + } + + #[test] + fn test_unrealized_pnl() { + let mut pm = PositionManager::new("TEST".to_string()); + + pm.open_position(0, 1000, 100.0, 10.0, Direction::Long, None, None); + + // Price up + let pnl = pm.unrealized_pnl(110.0); + assert!((pnl - 100.0).abs() < 1e-10); // (110 - 100) * 10 = 100 + + // Price down + let pnl = pm.unrealized_pnl(95.0); + assert!((pnl - (-50.0)).abs() < 1e-10); // (95 - 100) * 10 = -50 + } +} diff --git a/src/python/bindings.rs b/src/python/bindings.rs new file mode 100644 index 0000000..c548408 --- /dev/null +++ b/src/python/bindings.rs @@ -0,0 +1,988 @@ +//! PyO3 function bindings for RaptorBT. + +use numpy::{PyArray1, PyReadonlyArray1}; +use pyo3::prelude::*; + +use crate::core::types::{ + BacktestConfig, CompiledSignals, Direction, OhlcvData, StopConfig, TargetConfig, +}; +use crate::indicators; +use crate::signals::synchronizer::SyncMode; +use crate::strategies::basket::{BasketBacktest, BasketConfig}; +use crate::strategies::multi::{CombineMode, MultiStrategyBacktest, MultiStrategyConfig}; +use crate::strategies::options::{ + OptionType, OptionsBacktest, OptionsConfig, SizeType, StrikeSelection, +}; +use crate::strategies::pairs::{PairsBacktest, PairsConfig}; +use crate::strategies::single::SingleBacktest; + +use super::numpy_bridge::*; + +// ============================================================================ +// Configuration Classes +// ============================================================================ + +/// Python-exposed backtest configuration. +#[pyclass] +#[derive(Debug, Clone)] +pub struct PyBacktestConfig { + #[pyo3(get, set)] + pub initial_capital: f64, + #[pyo3(get, set)] + pub fees: f64, + #[pyo3(get, set)] + pub slippage: f64, + #[pyo3(get, set)] + pub upon_bar_close: bool, + stop_config: StopConfig, + target_config: TargetConfig, +} + +#[pymethods] +impl PyBacktestConfig { + #[new] + #[pyo3(signature = (initial_capital=100000.0, fees=0.001, slippage=0.0, upon_bar_close=true))] + fn new(initial_capital: f64, fees: f64, slippage: f64, upon_bar_close: bool) -> Self { + Self { + initial_capital, + fees, + slippage, + upon_bar_close, + stop_config: StopConfig::None, + target_config: TargetConfig::None, + } + } + + /// Set fixed percentage stop-loss. + fn set_fixed_stop(&mut self, percent: f64) { + self.stop_config = StopConfig::Fixed { percent }; + } + + /// Set ATR-based stop-loss. + fn set_atr_stop(&mut self, multiplier: f64, period: usize) { + self.stop_config = StopConfig::Atr { multiplier, period }; + } + + /// Set trailing stop-loss. + fn set_trailing_stop(&mut self, percent: f64) { + self.stop_config = StopConfig::Trailing { percent }; + } + + /// Set fixed percentage take-profit. + fn set_fixed_target(&mut self, percent: f64) { + self.target_config = TargetConfig::Fixed { percent }; + } + + /// Set ATR-based take-profit. + fn set_atr_target(&mut self, multiplier: f64, period: usize) { + self.target_config = TargetConfig::Atr { multiplier, period }; + } + + /// Set risk-reward based take-profit. + fn set_risk_reward_target(&mut self, ratio: f64) { + self.target_config = TargetConfig::RiskReward { ratio }; + } +} + +impl From<&PyBacktestConfig> for BacktestConfig { + fn from(py_config: &PyBacktestConfig) -> Self { + BacktestConfig { + initial_capital: py_config.initial_capital, + fees: py_config.fees, + slippage: py_config.slippage, + stop: py_config.stop_config, + target: py_config.target_config, + upon_bar_close: py_config.upon_bar_close, + } + } +} + +/// Python-exposed stop configuration. +#[pyclass] +#[derive(Debug, Clone)] +pub struct PyStopConfig { + #[pyo3(get, set)] + pub stop_type: String, + #[pyo3(get, set)] + pub percent: Option, + #[pyo3(get, set)] + pub multiplier: Option, + #[pyo3(get, set)] + pub period: Option, +} + +#[pymethods] +impl PyStopConfig { + #[new] + fn new() -> Self { + Self { + stop_type: "none".to_string(), + percent: None, + multiplier: None, + period: None, + } + } + + #[staticmethod] + fn fixed(percent: f64) -> Self { + Self { + stop_type: "fixed".to_string(), + percent: Some(percent), + multiplier: None, + period: None, + } + } + + #[staticmethod] + fn atr(multiplier: f64, period: usize) -> Self { + Self { + stop_type: "atr".to_string(), + percent: None, + multiplier: Some(multiplier), + period: Some(period), + } + } + + #[staticmethod] + fn trailing(percent: f64) -> Self { + Self { + stop_type: "trailing".to_string(), + percent: Some(percent), + multiplier: None, + period: None, + } + } +} + +/// Python-exposed target configuration. +#[pyclass] +#[derive(Debug, Clone)] +pub struct PyTargetConfig { + #[pyo3(get, set)] + pub target_type: String, + #[pyo3(get, set)] + pub percent: Option, + #[pyo3(get, set)] + pub multiplier: Option, + #[pyo3(get, set)] + pub period: Option, + #[pyo3(get, set)] + pub ratio: Option, +} + +#[pymethods] +impl PyTargetConfig { + #[new] + fn new() -> Self { + Self { + target_type: "none".to_string(), + percent: None, + multiplier: None, + period: None, + ratio: None, + } + } + + #[staticmethod] + fn fixed(percent: f64) -> Self { + Self { + target_type: "fixed".to_string(), + percent: Some(percent), + multiplier: None, + period: None, + ratio: None, + } + } + + #[staticmethod] + fn atr(multiplier: f64, period: usize) -> Self { + Self { + target_type: "atr".to_string(), + percent: None, + multiplier: Some(multiplier), + period: Some(period), + ratio: None, + } + } + + #[staticmethod] + fn risk_reward(ratio: f64) -> Self { + Self { + target_type: "risk_reward".to_string(), + percent: None, + multiplier: None, + period: None, + ratio: Some(ratio), + } + } +} + +// ============================================================================ +// Result Classes +// ============================================================================ + +/// Python-exposed trade. +#[pyclass] +#[derive(Debug, Clone)] +pub struct PyTrade { + #[pyo3(get)] + pub id: u64, + #[pyo3(get)] + pub symbol: String, + #[pyo3(get)] + pub entry_idx: usize, + #[pyo3(get)] + pub exit_idx: usize, + #[pyo3(get)] + pub entry_price: f64, + #[pyo3(get)] + pub exit_price: f64, + #[pyo3(get)] + pub size: f64, + #[pyo3(get)] + pub direction: i32, + #[pyo3(get)] + pub pnl: f64, + #[pyo3(get)] + pub return_pct: f64, + #[pyo3(get)] + pub entry_time: i64, + #[pyo3(get)] + pub exit_time: i64, + #[pyo3(get)] + pub fees: f64, + #[pyo3(get)] + pub exit_reason: String, +} + +#[pymethods] +impl PyTrade { + fn __repr__(&self) -> String { + format!( + "Trade(symbol={}, entry={:.2}, exit={:.2}, pnl={:.2}, return={:.2}%)", + self.symbol, self.entry_price, self.exit_price, self.pnl, self.return_pct + ) + } +} + +/// Python-exposed backtest metrics. +#[pyclass] +#[derive(Debug, Clone)] +pub struct PyBacktestMetrics { + #[pyo3(get)] + pub total_return_pct: f64, + #[pyo3(get)] + pub sharpe_ratio: f64, + #[pyo3(get)] + pub sortino_ratio: f64, + #[pyo3(get)] + pub calmar_ratio: f64, + #[pyo3(get)] + pub omega_ratio: f64, + #[pyo3(get)] + pub max_drawdown_pct: f64, + #[pyo3(get)] + pub max_drawdown_duration: usize, + #[pyo3(get)] + pub win_rate_pct: f64, + #[pyo3(get)] + pub profit_factor: f64, + #[pyo3(get)] + pub expectancy: f64, + #[pyo3(get)] + pub sqn: f64, + #[pyo3(get)] + pub total_trades: usize, + #[pyo3(get)] + pub total_closed_trades: usize, + #[pyo3(get)] + pub total_open_trades: usize, + #[pyo3(get)] + pub open_trade_pnl: f64, + #[pyo3(get)] + pub winning_trades: usize, + #[pyo3(get)] + pub losing_trades: usize, + #[pyo3(get)] + pub start_value: f64, + #[pyo3(get)] + pub end_value: f64, + #[pyo3(get)] + pub total_fees_paid: f64, + #[pyo3(get)] + pub best_trade_pct: f64, + #[pyo3(get)] + pub worst_trade_pct: f64, + #[pyo3(get)] + pub avg_trade_return_pct: f64, + #[pyo3(get)] + pub avg_win_pct: f64, + #[pyo3(get)] + pub avg_loss_pct: f64, + #[pyo3(get)] + pub avg_winning_duration: f64, + #[pyo3(get)] + pub avg_losing_duration: f64, + #[pyo3(get)] + pub max_consecutive_wins: usize, + #[pyo3(get)] + pub max_consecutive_losses: usize, + #[pyo3(get)] + pub avg_holding_period: f64, + #[pyo3(get)] + pub exposure_pct: f64, +} + +#[pymethods] +impl PyBacktestMetrics { + fn __repr__(&self) -> String { + format!( + "BacktestMetrics(return={:.2}%, sharpe={:.2}, max_dd={:.2}%, trades={})", + self.total_return_pct, self.sharpe_ratio, self.max_drawdown_pct, self.total_trades + ) + } + + /// Convert to dictionary matching VectorBT stats() format. + fn to_dict(&self, py: Python) -> PyResult { + let dict = pyo3::types::PyDict::new(py); + dict.set_item("Start Value", self.start_value)?; + dict.set_item("End Value", self.end_value)?; + dict.set_item("Total Return [%]", self.total_return_pct)?; + dict.set_item("Total Fees Paid", self.total_fees_paid)?; + dict.set_item("Max Drawdown [%]", self.max_drawdown_pct)?; + dict.set_item("Max Drawdown Duration", self.max_drawdown_duration)?; + dict.set_item("Total Trades", self.total_trades)?; + dict.set_item("Total Closed Trades", self.total_closed_trades)?; + dict.set_item("Total Open Trades", self.total_open_trades)?; + dict.set_item("Open Trade PnL", self.open_trade_pnl)?; + dict.set_item("Win Rate [%]", self.win_rate_pct)?; + dict.set_item("Best Trade [%]", self.best_trade_pct)?; + dict.set_item("Worst Trade [%]", self.worst_trade_pct)?; + dict.set_item("Avg Winning Trade [%]", self.avg_win_pct)?; + dict.set_item("Avg Losing Trade [%]", self.avg_loss_pct)?; + dict.set_item("Avg Winning Trade Duration", self.avg_winning_duration)?; + dict.set_item("Avg Losing Trade Duration", self.avg_losing_duration)?; + dict.set_item("Profit Factor", self.profit_factor)?; + dict.set_item("Expectancy", self.expectancy)?; + dict.set_item("SQN", self.sqn)?; + dict.set_item("Sharpe Ratio", self.sharpe_ratio)?; + dict.set_item("Sortino Ratio", self.sortino_ratio)?; + dict.set_item("Calmar Ratio", self.calmar_ratio)?; + dict.set_item("Omega Ratio", self.omega_ratio)?; + Ok(dict.into()) + } +} + +/// Python-exposed backtest result. +#[pyclass] +#[derive(Debug, Clone)] +pub struct PyBacktestResult { + #[pyo3(get)] + pub metrics: PyBacktestMetrics, + equity_curve: Vec, + drawdown_curve: Vec, + trades: Vec, + returns: Vec, +} + +#[pymethods] +impl PyBacktestResult { + /// Get equity curve as numpy array. + fn equity_curve<'py>(&self, py: Python<'py>) -> &'py PyArray1 { + vec_to_numpy_f64(py, self.equity_curve.clone()) + } + + /// Get drawdown curve as numpy array. + fn drawdown_curve<'py>(&self, py: Python<'py>) -> &'py PyArray1 { + vec_to_numpy_f64(py, self.drawdown_curve.clone()) + } + + /// Get returns as numpy array. + fn returns<'py>(&self, py: Python<'py>) -> &'py PyArray1 { + vec_to_numpy_f64(py, self.returns.clone()) + } + + /// Get list of trades. + fn trades(&self) -> Vec { + self.trades.clone() + } + + fn __repr__(&self) -> String { + format!( + "BacktestResult(return={:.2}%, trades={}, max_dd={:.2}%)", + self.metrics.total_return_pct, self.metrics.total_trades, self.metrics.max_drawdown_pct + ) + } +} + +// ============================================================================ +// Backtest Functions +// ============================================================================ + +/// Run single instrument backtest. +#[pyfunction] +#[pyo3(signature = (timestamps, open, high, low, close, volume, entries, exits, direction=1, weight=1.0, symbol="UNKNOWN", config=None, position_sizes=None))] +pub fn run_single_backtest<'py>( + _py: Python<'py>, + timestamps: PyReadonlyArray1, + open: PyReadonlyArray1, + high: PyReadonlyArray1, + low: PyReadonlyArray1, + close: PyReadonlyArray1, + volume: PyReadonlyArray1, + entries: PyReadonlyArray1, + exits: PyReadonlyArray1, + direction: i32, + weight: f64, + symbol: &str, + config: Option<&PyBacktestConfig>, + position_sizes: Option>, +) -> PyResult { + let ohlcv = OhlcvData { + timestamps: numpy_to_vec_i64(timestamps), + open: numpy_to_vec_f64(open), + high: numpy_to_vec_f64(high), + low: numpy_to_vec_f64(low), + close: numpy_to_vec_f64(close), + volume: numpy_to_vec_f64(volume), + }; + + let dir = Direction::from_int(direction).unwrap_or(Direction::Long); + + let signals = CompiledSignals { + symbol: symbol.to_string(), + entries: numpy_to_vec_bool(entries), + exits: numpy_to_vec_bool(exits), + position_sizes: position_sizes.map(numpy_to_vec_f64), + direction: dir, + weight, + }; + + let rust_config = config.map(|c| BacktestConfig::from(c)).unwrap_or_default(); + + let backtest = SingleBacktest::new(rust_config); + let result = backtest.run(&ohlcv, &signals); + + Ok(convert_result(result)) +} + +/// Run basket/collective backtest. +#[pyfunction] +#[pyo3(signature = (instruments, config=None, sync_mode="all"))] +pub fn run_basket_backtest<'py>( + _py: Python<'py>, + instruments: Vec<( + PyReadonlyArray1, + PyReadonlyArray1, + PyReadonlyArray1, + PyReadonlyArray1, + PyReadonlyArray1, + PyReadonlyArray1, + PyReadonlyArray1, + PyReadonlyArray1, + i32, + f64, + String, + )>, + config: Option<&PyBacktestConfig>, + sync_mode: &str, +) -> PyResult { + let rust_instruments: Vec<(OhlcvData, CompiledSignals)> = instruments + .into_iter() + .map(|(ts, o, h, l, c, v, entries, exits, dir, weight, sym)| { + let ohlcv = OhlcvData { + timestamps: numpy_to_vec_i64(ts), + open: numpy_to_vec_f64(o), + high: numpy_to_vec_f64(h), + low: numpy_to_vec_f64(l), + close: numpy_to_vec_f64(c), + volume: numpy_to_vec_f64(v), + }; + let signals = CompiledSignals { + symbol: sym, + entries: numpy_to_vec_bool(entries), + exits: numpy_to_vec_bool(exits), + position_sizes: None, + direction: Direction::from_int(dir).unwrap_or(Direction::Long), + weight, + }; + (ohlcv, signals) + }) + .collect(); + + let mode = match sync_mode { + "any" => SyncMode::Any, + "majority" => SyncMode::Majority, + "master" => SyncMode::Master, + _ => SyncMode::All, + }; + + let basket_config = BasketConfig { + base: config.map(|c| BacktestConfig::from(c)).unwrap_or_default(), + sync_mode: mode, + ..Default::default() + }; + + let backtest = BasketBacktest::new(basket_config); + let result = backtest.run(&rust_instruments); + + Ok(convert_result(result)) +} + +/// Run options backtest. +#[pyfunction] +#[pyo3(signature = (timestamps, open, high, low, close, volume, option_prices, entries, exits, direction=1, symbol="OPTION", config=None, option_type="call", strike_selection="atm", size_type="percent", size_value=1.0, lot_size=1, strike_interval=50.0))] +pub fn run_options_backtest<'py>( + _py: Python<'py>, + timestamps: PyReadonlyArray1, + open: PyReadonlyArray1, + high: PyReadonlyArray1, + low: PyReadonlyArray1, + close: PyReadonlyArray1, + volume: PyReadonlyArray1, + option_prices: PyReadonlyArray1, + entries: PyReadonlyArray1, + exits: PyReadonlyArray1, + direction: i32, + symbol: &str, + config: Option<&PyBacktestConfig>, + option_type: &str, + strike_selection: &str, + size_type: &str, + size_value: f64, + lot_size: usize, + strike_interval: f64, +) -> PyResult { + let ohlcv = OhlcvData { + timestamps: numpy_to_vec_i64(timestamps), + open: numpy_to_vec_f64(open), + high: numpy_to_vec_f64(high), + low: numpy_to_vec_f64(low), + close: numpy_to_vec_f64(close), + volume: numpy_to_vec_f64(volume), + }; + + let opt_prices = numpy_to_vec_f64(option_prices); + + let dir = Direction::from_int(direction).unwrap_or(Direction::Long); + + let signals = CompiledSignals { + symbol: symbol.to_string(), + entries: numpy_to_vec_bool(entries), + exits: numpy_to_vec_bool(exits), + position_sizes: None, + direction: dir, + weight: 1.0, + }; + + let opt_type = match option_type { + "put" => OptionType::Put, + _ => OptionType::Call, + }; + + let strike_sel = match strike_selection { + "otm1" => StrikeSelection::Otm(1), + "otm2" => StrikeSelection::Otm(2), + "itm1" => StrikeSelection::Itm(1), + "itm2" => StrikeSelection::Itm(2), + _ => StrikeSelection::Atm, + }; + + let size = match size_type { + "contracts" => SizeType::Contracts(size_value as usize), + "notional" => SizeType::Notional(size_value), + "risk" => SizeType::RiskPercent(size_value), + _ => SizeType::Percent(size_value), + }; + + let options_config = OptionsConfig { + base: config.map(|c| BacktestConfig::from(c)).unwrap_or_default(), + option_type: opt_type, + strike_selection: strike_sel, + size_type: size, + lot_size, + strike_interval, + target_dte: None, + }; + + let backtest = OptionsBacktest::new(options_config); + let result = backtest.run(&ohlcv, &opt_prices, &signals); + + Ok(convert_result(result)) +} + +/// Run pairs trading backtest. +#[pyfunction] +#[pyo3(signature = (leg1_timestamps, leg1_open, leg1_high, leg1_low, leg1_close, leg1_volume, leg2_timestamps, leg2_open, leg2_high, leg2_low, leg2_close, leg2_volume, entries, exits, direction=1, symbol="PAIR", config=None, hedge_ratio=1.0, dynamic_hedge=false))] +pub fn run_pairs_backtest<'py>( + _py: Python<'py>, + leg1_timestamps: PyReadonlyArray1, + leg1_open: PyReadonlyArray1, + leg1_high: PyReadonlyArray1, + leg1_low: PyReadonlyArray1, + leg1_close: PyReadonlyArray1, + leg1_volume: PyReadonlyArray1, + leg2_timestamps: PyReadonlyArray1, + leg2_open: PyReadonlyArray1, + leg2_high: PyReadonlyArray1, + leg2_low: PyReadonlyArray1, + leg2_close: PyReadonlyArray1, + leg2_volume: PyReadonlyArray1, + entries: PyReadonlyArray1, + exits: PyReadonlyArray1, + direction: i32, + symbol: &str, + config: Option<&PyBacktestConfig>, + hedge_ratio: f64, + dynamic_hedge: bool, +) -> PyResult { + let leg1_ohlcv = OhlcvData { + timestamps: numpy_to_vec_i64(leg1_timestamps), + open: numpy_to_vec_f64(leg1_open), + high: numpy_to_vec_f64(leg1_high), + low: numpy_to_vec_f64(leg1_low), + close: numpy_to_vec_f64(leg1_close), + volume: numpy_to_vec_f64(leg1_volume), + }; + + let leg2_ohlcv = OhlcvData { + timestamps: numpy_to_vec_i64(leg2_timestamps), + open: numpy_to_vec_f64(leg2_open), + high: numpy_to_vec_f64(leg2_high), + low: numpy_to_vec_f64(leg2_low), + close: numpy_to_vec_f64(leg2_close), + volume: numpy_to_vec_f64(leg2_volume), + }; + + let dir = Direction::from_int(direction).unwrap_or(Direction::Long); + + let signals = CompiledSignals { + symbol: symbol.to_string(), + entries: numpy_to_vec_bool(entries), + exits: numpy_to_vec_bool(exits), + position_sizes: None, + direction: dir, + weight: 1.0, + }; + + let pairs_config = PairsConfig { + base: config.map(|c| BacktestConfig::from(c)).unwrap_or_default(), + hedge_ratio, + dynamic_hedge, + ..Default::default() + }; + + let backtest = PairsBacktest::new(pairs_config); + let result = backtest.run(&leg1_ohlcv, &leg2_ohlcv, &signals); + + Ok(convert_result(result)) +} + +/// Run multi-strategy backtest. +#[pyfunction] +#[pyo3(signature = (timestamps, open, high, low, close, volume, strategies, config=None, combine_mode="any"))] +pub fn run_multi_backtest<'py>( + _py: Python<'py>, + timestamps: PyReadonlyArray1, + open: PyReadonlyArray1, + high: PyReadonlyArray1, + low: PyReadonlyArray1, + close: PyReadonlyArray1, + volume: PyReadonlyArray1, + strategies: Vec<( + PyReadonlyArray1, + PyReadonlyArray1, + i32, + f64, + String, + )>, + config: Option<&PyBacktestConfig>, + combine_mode: &str, +) -> PyResult { + let ohlcv = OhlcvData { + timestamps: numpy_to_vec_i64(timestamps), + open: numpy_to_vec_f64(open), + high: numpy_to_vec_f64(high), + low: numpy_to_vec_f64(low), + close: numpy_to_vec_f64(close), + volume: numpy_to_vec_f64(volume), + }; + + let rust_strategies: Vec = strategies + .into_iter() + .map(|(entries, exits, dir, weight, symbol)| CompiledSignals { + symbol, + entries: numpy_to_vec_bool(entries), + exits: numpy_to_vec_bool(exits), + position_sizes: None, + direction: Direction::from_int(dir).unwrap_or(Direction::Long), + weight, + }) + .collect(); + + let mode = match combine_mode { + "all" => CombineMode::All, + "majority" => CombineMode::Majority, + "independent" => CombineMode::Independent, + "weighted" => CombineMode::Weighted, + _ => CombineMode::Any, + }; + + let multi_config = MultiStrategyConfig { + base: config.map(|c| BacktestConfig::from(c)).unwrap_or_default(), + combine_mode: mode, + ..Default::default() + }; + + let backtest = MultiStrategyBacktest::new(multi_config); + let result = backtest.run(&ohlcv, &rust_strategies); + + Ok(convert_result(result)) +} + +// ============================================================================ +// Indicator Functions +// ============================================================================ + +/// Simple Moving Average. +#[pyfunction] +pub fn sma<'py>( + py: Python<'py>, + data: PyReadonlyArray1, + period: usize, +) -> PyResult<&'py PyArray1> { + let vec = numpy_to_vec_f64(data); + let result = indicators::trend::sma(&vec, period) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(vec_to_numpy_f64(py, result)) +} + +/// Exponential Moving Average. +#[pyfunction] +pub fn ema<'py>( + py: Python<'py>, + data: PyReadonlyArray1, + period: usize, +) -> PyResult<&'py PyArray1> { + let vec = numpy_to_vec_f64(data); + let result = indicators::trend::ema(&vec, period) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(vec_to_numpy_f64(py, result)) +} + +/// Relative Strength Index. +#[pyfunction] +pub fn rsi<'py>( + py: Python<'py>, + data: PyReadonlyArray1, + period: usize, +) -> PyResult<&'py PyArray1> { + let vec = numpy_to_vec_f64(data); + let result = indicators::momentum::rsi(&vec, period) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(vec_to_numpy_f64(py, result)) +} + +/// MACD indicator. +#[pyfunction] +#[pyo3(signature = (data, fast_period=12, slow_period=26, signal_period=9))] +pub fn macd<'py>( + py: Python<'py>, + data: PyReadonlyArray1, + fast_period: usize, + slow_period: usize, + signal_period: usize, +) -> PyResult<(&'py PyArray1, &'py PyArray1, &'py PyArray1)> { + let vec = numpy_to_vec_f64(data); + let result = indicators::momentum::macd(&vec, fast_period, slow_period, signal_period) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(( + vec_to_numpy_f64(py, result.macd_line), + vec_to_numpy_f64(py, result.signal_line), + vec_to_numpy_f64(py, result.histogram), + )) +} + +/// Stochastic oscillator. +#[pyfunction] +#[pyo3(signature = (high, low, close, k_period=14, d_period=3))] +pub fn stochastic<'py>( + py: Python<'py>, + high: PyReadonlyArray1, + low: PyReadonlyArray1, + close: PyReadonlyArray1, + k_period: usize, + d_period: usize, +) -> PyResult<(&'py PyArray1, &'py PyArray1)> { + let h = numpy_to_vec_f64(high); + let l = numpy_to_vec_f64(low); + let c = numpy_to_vec_f64(close); + let result = indicators::momentum::stochastic(&h, &l, &c, k_period, d_period) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(( + vec_to_numpy_f64(py, result.k), + vec_to_numpy_f64(py, result.d), + )) +} + +/// Average True Range. +#[pyfunction] +pub fn atr<'py>( + py: Python<'py>, + high: PyReadonlyArray1, + low: PyReadonlyArray1, + close: PyReadonlyArray1, + period: usize, +) -> PyResult<&'py PyArray1> { + let h = numpy_to_vec_f64(high); + let l = numpy_to_vec_f64(low); + let c = numpy_to_vec_f64(close); + let result = indicators::volatility::atr(&h, &l, &c, period) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(vec_to_numpy_f64(py, result)) +} + +/// Bollinger Bands. +#[pyfunction] +#[pyo3(signature = (data, period=20, std_dev=2.0))] +pub fn bollinger_bands<'py>( + py: Python<'py>, + data: PyReadonlyArray1, + period: usize, + std_dev: f64, +) -> PyResult<(&'py PyArray1, &'py PyArray1, &'py PyArray1)> { + let vec = numpy_to_vec_f64(data); + let result = indicators::volatility::bollinger_bands(&vec, period, std_dev) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(( + vec_to_numpy_f64(py, result.upper), + vec_to_numpy_f64(py, result.middle), + vec_to_numpy_f64(py, result.lower), + )) +} + +/// Average Directional Index. +#[pyfunction] +pub fn adx<'py>( + py: Python<'py>, + high: PyReadonlyArray1, + low: PyReadonlyArray1, + close: PyReadonlyArray1, + period: usize, +) -> PyResult<&'py PyArray1> { + let h = numpy_to_vec_f64(high); + let l = numpy_to_vec_f64(low); + let c = numpy_to_vec_f64(close); + let result = indicators::strength::adx(&h, &l, &c, period) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(vec_to_numpy_f64(py, result)) +} + +/// Volume Weighted Average Price. +#[pyfunction] +pub fn vwap<'py>( + py: Python<'py>, + high: PyReadonlyArray1, + low: PyReadonlyArray1, + close: PyReadonlyArray1, + volume: PyReadonlyArray1, +) -> PyResult<&'py PyArray1> { + let h = numpy_to_vec_f64(high); + let l = numpy_to_vec_f64(low); + let c = numpy_to_vec_f64(close); + let v = numpy_to_vec_f64(volume); + let result = indicators::volume::vwap(&h, &l, &c, &v) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(vec_to_numpy_f64(py, result)) +} + +/// Supertrend indicator. +#[pyfunction] +#[pyo3(signature = (high, low, close, period=10, multiplier=3.0))] +pub fn supertrend<'py>( + py: Python<'py>, + high: PyReadonlyArray1, + low: PyReadonlyArray1, + close: PyReadonlyArray1, + period: usize, + multiplier: f64, +) -> PyResult<(&'py PyArray1, &'py PyArray1)> { + let h = numpy_to_vec_f64(high); + let l = numpy_to_vec_f64(low); + let c = numpy_to_vec_f64(close); + let result = indicators::trend::supertrend(&h, &l, &c, period, multiplier) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + + let direction_array = PyArray1::from_vec(py, result.direction); + Ok((vec_to_numpy_f64(py, result.supertrend), direction_array)) +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/// Convert Rust BacktestResult to Python PyBacktestResult. +fn convert_result(result: crate::core::types::BacktestResult) -> PyBacktestResult { + let metrics = PyBacktestMetrics { + total_return_pct: result.metrics.total_return_pct, + sharpe_ratio: result.metrics.sharpe_ratio, + sortino_ratio: result.metrics.sortino_ratio, + calmar_ratio: result.metrics.calmar_ratio, + omega_ratio: result.metrics.omega_ratio, + max_drawdown_pct: result.metrics.max_drawdown_pct, + max_drawdown_duration: result.metrics.max_drawdown_duration, + win_rate_pct: result.metrics.win_rate_pct, + profit_factor: result.metrics.profit_factor, + expectancy: result.metrics.expectancy, + sqn: result.metrics.sqn, + total_trades: result.metrics.total_trades, + total_closed_trades: result.metrics.total_closed_trades, + total_open_trades: result.metrics.total_open_trades, + open_trade_pnl: result.metrics.open_trade_pnl, + winning_trades: result.metrics.winning_trades, + losing_trades: result.metrics.losing_trades, + start_value: result.metrics.start_value, + end_value: result.metrics.end_value, + total_fees_paid: result.metrics.total_fees_paid, + best_trade_pct: result.metrics.best_trade_pct, + worst_trade_pct: result.metrics.worst_trade_pct, + avg_trade_return_pct: result.metrics.avg_trade_return_pct, + avg_win_pct: result.metrics.avg_win_pct, + avg_loss_pct: result.metrics.avg_loss_pct, + avg_winning_duration: result.metrics.avg_winning_duration, + avg_losing_duration: result.metrics.avg_losing_duration, + max_consecutive_wins: result.metrics.max_consecutive_wins, + max_consecutive_losses: result.metrics.max_consecutive_losses, + avg_holding_period: result.metrics.avg_holding_period, + exposure_pct: result.metrics.exposure_pct, + }; + + let trades: Vec = result + .trades + .into_iter() + .map(|t| PyTrade { + id: t.id, + symbol: t.symbol, + entry_idx: t.entry_idx, + exit_idx: t.exit_idx, + entry_price: t.entry_price, + exit_price: t.exit_price, + size: t.size, + direction: t.direction as i32, + pnl: t.pnl, + return_pct: t.return_pct, + entry_time: t.entry_time, + exit_time: t.exit_time, + fees: t.fees, + exit_reason: format!("{:?}", t.exit_reason), + }) + .collect(); + + PyBacktestResult { + metrics, + equity_curve: result.equity_curve, + drawdown_curve: result.drawdown_curve, + trades, + returns: result.returns, + } +} diff --git a/src/python/mod.rs b/src/python/mod.rs new file mode 100644 index 0000000..c4527d9 --- /dev/null +++ b/src/python/mod.rs @@ -0,0 +1,4 @@ +//! Python bindings for RaptorBT. + +pub mod bindings; +pub mod numpy_bridge; diff --git a/src/python/numpy_bridge.rs b/src/python/numpy_bridge.rs new file mode 100644 index 0000000..9770b53 --- /dev/null +++ b/src/python/numpy_bridge.rs @@ -0,0 +1,34 @@ +//! Zero-copy numpy array interface. + +use numpy::{PyArray1, PyReadonlyArray1}; +use pyo3::prelude::*; + +/// Convert numpy array to Vec. +pub fn numpy_to_vec_f64(arr: PyReadonlyArray1) -> Vec { + arr.as_slice().unwrap().to_vec() +} + +/// Convert numpy array to Vec. +pub fn numpy_to_vec_i64(arr: PyReadonlyArray1) -> Vec { + arr.as_slice().unwrap().to_vec() +} + +/// Convert numpy bool array to Vec. +pub fn numpy_to_vec_bool(arr: PyReadonlyArray1) -> Vec { + arr.as_slice().unwrap().to_vec() +} + +/// Convert Vec to numpy array. +pub fn vec_to_numpy_f64<'py>(py: Python<'py>, vec: Vec) -> &'py PyArray1 { + PyArray1::from_vec(py, vec) +} + +/// Convert Vec to numpy array. +pub fn vec_to_numpy_i64<'py>(py: Python<'py>, vec: Vec) -> &'py PyArray1 { + PyArray1::from_vec(py, vec) +} + +/// Convert Vec to numpy array. +pub fn vec_to_numpy_bool<'py>(py: Python<'py>, vec: Vec) -> &'py PyArray1 { + PyArray1::from_vec(py, vec) +} diff --git a/src/signals/expression.rs b/src/signals/expression.rs new file mode 100644 index 0000000..ba7af07 --- /dev/null +++ b/src/signals/expression.rs @@ -0,0 +1,460 @@ +//! Expression evaluation for signal generation. +//! +//! Provides a Rust-native expression evaluator for generating trading signals +//! from indicator values. + +/// Comparison operators for signal generation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompareOp { + /// Greater than. + Gt, + /// Greater than or equal. + Gte, + /// Less than. + Lt, + /// Less than or equal. + Lte, + /// Equal (within tolerance). + Eq, + /// Not equal. + Ne, +} + +/// Crossover/crossunder detection. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CrossType { + /// Line A crosses above line B. + CrossOver, + /// Line A crosses below line B. + CrossUnder, +} + +/// Compare two series element-wise. +/// +/// # Arguments +/// * `a` - First series +/// * `b` - Second series +/// * `op` - Comparison operator +/// +/// # Returns +/// Boolean series indicating where comparison is true +pub fn compare(a: &[f64], b: &[f64], op: CompareOp) -> Vec { + let n = a.len(); + assert_eq!(n, b.len()); + + let tolerance = 1e-10; + + let mut result = vec![false; n]; + for i in 0..n { + if a[i].is_nan() || b[i].is_nan() { + continue; + } + result[i] = match op { + CompareOp::Gt => a[i] > b[i], + CompareOp::Gte => a[i] >= b[i], + CompareOp::Lt => a[i] < b[i], + CompareOp::Lte => a[i] <= b[i], + CompareOp::Eq => (a[i] - b[i]).abs() < tolerance, + CompareOp::Ne => (a[i] - b[i]).abs() >= tolerance, + }; + } + + result +} + +/// Compare series with a scalar value. +/// +/// # Arguments +/// * `a` - Series +/// * `value` - Scalar value to compare against +/// * `op` - Comparison operator +/// +/// # Returns +/// Boolean series indicating where comparison is true +pub fn compare_scalar(a: &[f64], value: f64, op: CompareOp) -> Vec { + let n = a.len(); + let tolerance = 1e-10; + + let mut result = vec![false; n]; + for i in 0..n { + if a[i].is_nan() { + continue; + } + result[i] = match op { + CompareOp::Gt => a[i] > value, + CompareOp::Gte => a[i] >= value, + CompareOp::Lt => a[i] < value, + CompareOp::Lte => a[i] <= value, + CompareOp::Eq => (a[i] - value).abs() < tolerance, + CompareOp::Ne => (a[i] - value).abs() >= tolerance, + }; + } + + result +} + +/// Detect crossover/crossunder between two series. +/// +/// Crossover: a crosses above b (a[i-1] < b[i-1] and a[i] > b[i]) +/// Crossunder: a crosses below b (a[i-1] > b[i-1] and a[i] < b[i]) +/// +/// # Arguments +/// * `a` - First series +/// * `b` - Second series +/// * `cross_type` - Type of cross to detect +/// +/// # Returns +/// Boolean series indicating where cross occurs +pub fn cross(a: &[f64], b: &[f64], cross_type: CrossType) -> Vec { + let n = a.len(); + assert_eq!(n, b.len()); + + let mut result = vec![false; n]; + if n < 2 { + return result; + } + + for i in 1..n { + if a[i].is_nan() || b[i].is_nan() || a[i - 1].is_nan() || b[i - 1].is_nan() { + continue; + } + + result[i] = match cross_type { + CrossType::CrossOver => a[i - 1] <= b[i - 1] && a[i] > b[i], + CrossType::CrossUnder => a[i - 1] >= b[i - 1] && a[i] < b[i], + }; + } + + result +} + +/// Detect crossover with a scalar value. +/// +/// # Arguments +/// * `a` - Series +/// * `value` - Scalar value to cross +/// * `cross_type` - Type of cross to detect +/// +/// # Returns +/// Boolean series indicating where cross occurs +pub fn cross_scalar(a: &[f64], value: f64, cross_type: CrossType) -> Vec { + let n = a.len(); + let mut result = vec![false; n]; + + if n < 2 { + return result; + } + + for i in 1..n { + if a[i].is_nan() || a[i - 1].is_nan() { + continue; + } + + result[i] = match cross_type { + CrossType::CrossOver => a[i - 1] <= value && a[i] > value, + CrossType::CrossUnder => a[i - 1] >= value && a[i] < value, + }; + } + + result +} + +/// Check if value is in a range. +/// +/// # Arguments +/// * `a` - Series +/// * `low` - Lower bound +/// * `high` - Upper bound +/// +/// # Returns +/// Boolean series indicating where value is in range [low, high] +pub fn in_range(a: &[f64], low: f64, high: f64) -> Vec { + let n = a.len(); + let mut result = vec![false; n]; + + for i in 0..n { + if a[i].is_nan() { + continue; + } + result[i] = a[i] >= low && a[i] <= high; + } + + result +} + +/// Check if series is rising (current > previous). +/// +/// # Arguments +/// * `a` - Series +/// * `periods` - Number of periods to look back (default: 1) +/// +/// # Returns +/// Boolean series indicating where value is rising +pub fn is_rising(a: &[f64], periods: usize) -> Vec { + let n = a.len(); + let mut result = vec![false; n]; + + if periods >= n { + return result; + } + + for i in periods..n { + if a[i].is_nan() || a[i - periods].is_nan() { + continue; + } + result[i] = a[i] > a[i - periods]; + } + + result +} + +/// Check if series is falling (current < previous). +/// +/// # Arguments +/// * `a` - Series +/// * `periods` - Number of periods to look back (default: 1) +/// +/// # Returns +/// Boolean series indicating where value is falling +pub fn is_falling(a: &[f64], periods: usize) -> Vec { + let n = a.len(); + let mut result = vec![false; n]; + + if periods >= n { + return result; + } + + for i in periods..n { + if a[i].is_nan() || a[i - periods].is_nan() { + continue; + } + result[i] = a[i] < a[i - periods]; + } + + result +} + +/// Check if value has been above a threshold for n consecutive bars. +/// +/// # Arguments +/// * `a` - Series +/// * `threshold` - Threshold value +/// * `consecutive` - Number of consecutive bars required +/// +/// # Returns +/// Boolean series indicating where condition is met +pub fn above_for(a: &[f64], threshold: f64, consecutive: usize) -> Vec { + let n = a.len(); + let mut result = vec![false; n]; + + if consecutive > n { + return result; + } + + for i in (consecutive - 1)..n { + let mut all_above = true; + for j in 0..consecutive { + let idx = i - j; + if a[idx].is_nan() || a[idx] <= threshold { + all_above = false; + break; + } + } + result[i] = all_above; + } + + result +} + +/// Check if value has been below a threshold for n consecutive bars. +/// +/// # Arguments +/// * `a` - Series +/// * `threshold` - Threshold value +/// * `consecutive` - Number of consecutive bars required +/// +/// # Returns +/// Boolean series indicating where condition is met +pub fn below_for(a: &[f64], threshold: f64, consecutive: usize) -> Vec { + let n = a.len(); + let mut result = vec![false; n]; + + if consecutive > n { + return result; + } + + for i in (consecutive - 1)..n { + let mut all_below = true; + for j in 0..consecutive { + let idx = i - j; + if a[idx].is_nan() || a[idx] >= threshold { + all_below = false; + break; + } + } + result[i] = all_below; + } + + result +} + +/// Detect highest value in rolling window. +/// +/// # Arguments +/// * `a` - Series +/// * `window` - Window size +/// +/// # Returns +/// Boolean series indicating where current value is highest in window +pub fn is_highest(a: &[f64], window: usize) -> Vec { + let n = a.len(); + let mut result = vec![false; n]; + + if window > n || window == 0 { + return result; + } + + for i in (window - 1)..n { + let start = i + 1 - window; + let current = a[i]; + if current.is_nan() { + continue; + } + + let max_in_window = a[start..=i] + .iter() + .filter(|v| !v.is_nan()) + .fold(f64::NEG_INFINITY, |a, &b| a.max(b)); + + result[i] = (current - max_in_window).abs() < 1e-10; + } + + result +} + +/// Detect lowest value in rolling window. +/// +/// # Arguments +/// * `a` - Series +/// * `window` - Window size +/// +/// # Returns +/// Boolean series indicating where current value is lowest in window +pub fn is_lowest(a: &[f64], window: usize) -> Vec { + let n = a.len(); + let mut result = vec![false; n]; + + if window > n || window == 0 { + return result; + } + + for i in (window - 1)..n { + let start = i + 1 - window; + let current = a[i]; + if current.is_nan() { + continue; + } + + let min_in_window = a[start..=i] + .iter() + .filter(|v| !v.is_nan()) + .fold(f64::INFINITY, |a, &b| a.min(b)); + + result[i] = (current - min_in_window).abs() < 1e-10; + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_compare() { + let a = vec![1.0, 2.0, 3.0, 4.0]; + let b = vec![2.0, 2.0, 2.0, 2.0]; + + let result = compare(&a, &b, CompareOp::Gt); + assert!(!result[0]); // 1 > 2 = false + assert!(!result[1]); // 2 > 2 = false + assert!(result[2]); // 3 > 2 = true + assert!(result[3]); // 4 > 2 = true + } + + #[test] + fn test_crossover() { + let a = vec![1.0, 1.5, 2.5, 3.0, 2.5]; + let b = vec![2.0, 2.0, 2.0, 2.0, 2.0]; + + let result = cross(&a, &b, CrossType::CrossOver); + assert!(!result[0]); // No previous + assert!(!result[1]); // 1.0 < 2.0, 1.5 < 2.0 - still below + assert!(result[2]); // 1.5 < 2.0, 2.5 > 2.0 - crossed over! + assert!(!result[3]); // 2.5 > 2.0, 3.0 > 2.0 - already above + assert!(!result[4]); // 3.0 > 2.0, 2.5 > 2.0 - still above + } + + #[test] + fn test_crossunder() { + let a = vec![3.0, 2.5, 1.5, 1.0, 1.5]; + let b = vec![2.0, 2.0, 2.0, 2.0, 2.0]; + + let result = cross(&a, &b, CrossType::CrossUnder); + assert!(!result[0]); // No previous + assert!(!result[1]); // 3.0 > 2.0, 2.5 > 2.0 - still above + assert!(result[2]); // 2.5 > 2.0, 1.5 < 2.0 - crossed under! + assert!(!result[3]); // 1.5 < 2.0, 1.0 < 2.0 - already below + assert!(!result[4]); // 1.0 < 2.0, 1.5 < 2.0 - still below + } + + #[test] + fn test_in_range() { + let a = vec![1.0, 2.0, 3.0, 4.0, 5.0]; + + let result = in_range(&a, 2.0, 4.0); + assert!(!result[0]); // 1 not in [2, 4] + assert!(result[1]); // 2 in [2, 4] + assert!(result[2]); // 3 in [2, 4] + assert!(result[3]); // 4 in [2, 4] + assert!(!result[4]); // 5 not in [2, 4] + } + + #[test] + fn test_is_rising() { + let a = vec![1.0, 2.0, 3.0, 2.5, 3.5]; + + let result = is_rising(&a, 1); + assert!(!result[0]); // No previous + assert!(result[1]); // 2 > 1 + assert!(result[2]); // 3 > 2 + assert!(!result[3]); // 2.5 < 3 + assert!(result[4]); // 3.5 > 2.5 + } + + #[test] + fn test_above_for() { + let a = vec![1.0, 3.0, 3.5, 4.0, 2.0, 3.0]; + let threshold = 2.5; + + let result = above_for(&a, threshold, 3); + assert!(!result[0]); + assert!(!result[1]); + assert!(!result[2]); // 1.0 < 2.5 + assert!(result[3]); // 3.0, 3.5, 4.0 all > 2.5 + assert!(!result[4]); // 2.0 < 2.5 + assert!(!result[5]); + } + + #[test] + fn test_is_highest() { + let a = vec![1.0, 3.0, 2.0, 4.0, 3.5]; + + let result = is_highest(&a, 3); + assert!(!result[0]); + assert!(!result[1]); + assert!(result[2] == false); // 2.0 is not highest in [1.0, 3.0, 2.0] + assert!(result[3]); // 4.0 is highest in [3.0, 2.0, 4.0] + assert!(!result[4]); // 3.5 is not highest in [2.0, 4.0, 3.5] + } +} diff --git a/src/signals/mod.rs b/src/signals/mod.rs new file mode 100644 index 0000000..e16b71a --- /dev/null +++ b/src/signals/mod.rs @@ -0,0 +1,10 @@ +//! Signal processing for RaptorBT. +//! +//! This module handles signal cleaning, synchronization, and expression evaluation. + +pub mod expression; +pub mod processor; +pub mod synchronizer; + +pub use processor::SignalProcessor; +pub use synchronizer::{SignalSynchronizer, SyncMode}; diff --git a/src/signals/processor.rs b/src/signals/processor.rs new file mode 100644 index 0000000..a1097c3 --- /dev/null +++ b/src/signals/processor.rs @@ -0,0 +1,449 @@ +//! Signal processor for cleaning entry/exit signals. +//! +//! Ensures proper alternation between entries and exits to prevent +//! overlapping positions or orphaned signals. + +use crate::core::types::Direction; + +/// Signal processor for cleaning raw entry/exit signals. +#[derive(Debug, Clone)] +pub struct SignalProcessor { + /// Whether to allow multiple entries before an exit (pyramiding). + pub allow_pyramiding: bool, + /// Maximum number of pyramid entries. + pub max_pyramid_entries: usize, +} + +impl Default for SignalProcessor { + fn default() -> Self { + Self { + allow_pyramiding: false, + max_pyramid_entries: 1, + } + } +} + +impl SignalProcessor { + /// Create a new signal processor. + pub fn new() -> Self { + Self::default() + } + + /// Enable pyramiding with a maximum number of entries. + pub fn with_pyramiding(mut self, max_entries: usize) -> Self { + self.allow_pyramiding = max_entries > 1; + self.max_pyramid_entries = max_entries; + self + } + + /// Clean entry/exit signals to ensure proper alternation. + /// + /// Rules (matching VectorBT behavior): + /// 1. First signal must be an entry + /// 2. After an entry, ignore further entries (unless pyramiding) + /// 3. After an exit, ignore further exits + /// 4. Entries and exits must alternate properly + /// 5. Same-bar conflict: If both entry AND exit signals are True on the same bar + /// when in position, VectorBT stays in position (ignores the exit). + /// This matches VectorBT's "entry takes priority" behavior. + /// + /// # Arguments + /// * `entries` - Raw entry signals + /// * `exits` - Raw exit signals + /// + /// # Returns + /// Tuple of (cleaned_entries, cleaned_exits) + pub fn clean_signals(&self, entries: &[bool], exits: &[bool]) -> (Vec, Vec) { + let n = entries.len(); + assert_eq!( + n, + exits.len(), + "Entry and exit arrays must have same length" + ); + + let mut clean_entries = vec![false; n]; + let mut clean_exits = vec![false; n]; + + if n == 0 { + return (clean_entries, clean_exits); + } + + let mut in_position = false; + let mut position_count = 0; + + for i in 0..n { + if !in_position { + // Not in position - looking for entry + if entries[i] { + clean_entries[i] = true; + in_position = true; + position_count = 1; + } + // Ignore exits when not in position + } else { + // In position - looking for exit (or pyramid entry) + // VectorBT behavior: If both entry and exit are True, stay in position + // (entry signal "cancels" the exit signal) + if exits[i] && !entries[i] { + // Only exit if there's no conflicting entry signal + clean_exits[i] = true; + if self.allow_pyramiding { + position_count -= 1; + if position_count == 0 { + in_position = false; + } + } else { + in_position = false; + position_count = 0; + } + } else if entries[i] + && self.allow_pyramiding + && position_count < self.max_pyramid_entries + { + // Pyramid entry + clean_entries[i] = true; + position_count += 1; + } + // If both entry and exit are True, we stay in position (ignore both) + // If only entry is True and not pyramiding, ignore entry (already in position) + } + } + + (clean_entries, clean_exits) + } + + /// Clean signals with direction awareness (for strategies that can go long/short). + /// + /// # Arguments + /// * `long_entries` - Long entry signals + /// * `long_exits` - Long exit signals + /// * `short_entries` - Short entry signals + /// * `short_exits` - Short exit signals + /// + /// # Returns + /// Tuple of (clean_long_entries, clean_long_exits, clean_short_entries, clean_short_exits) + pub fn clean_signals_bidirectional( + &self, + long_entries: &[bool], + long_exits: &[bool], + short_entries: &[bool], + short_exits: &[bool], + ) -> (Vec, Vec, Vec, Vec) { + let n = long_entries.len(); + assert_eq!(n, long_exits.len()); + assert_eq!(n, short_entries.len()); + assert_eq!(n, short_exits.len()); + + let mut clean_long_entries = vec![false; n]; + let mut clean_long_exits = vec![false; n]; + let mut clean_short_entries = vec![false; n]; + let mut clean_short_exits = vec![false; n]; + + if n == 0 { + return ( + clean_long_entries, + clean_long_exits, + clean_short_entries, + clean_short_exits, + ); + } + + let mut current_direction: Option = None; + + for i in 0..n { + match current_direction { + None => { + // Not in any position - look for entry + if long_entries[i] { + clean_long_entries[i] = true; + current_direction = Some(Direction::Long); + } else if short_entries[i] { + clean_short_entries[i] = true; + current_direction = Some(Direction::Short); + } + } + Some(Direction::Long) => { + // In long position - look for exit or reversal + if long_exits[i] { + clean_long_exits[i] = true; + current_direction = None; + } else if short_entries[i] { + // Reversal: exit long and enter short + clean_long_exits[i] = true; + clean_short_entries[i] = true; + current_direction = Some(Direction::Short); + } + } + Some(Direction::Short) => { + // In short position - look for exit or reversal + if short_exits[i] { + clean_short_exits[i] = true; + current_direction = None; + } else if long_entries[i] { + // Reversal: exit short and enter long + clean_short_exits[i] = true; + clean_long_entries[i] = true; + current_direction = Some(Direction::Long); + } + } + } + } + + ( + clean_long_entries, + clean_long_exits, + clean_short_entries, + clean_short_exits, + ) + } + + /// Generate exit-on-opposite-entry signals. + /// + /// Useful for strategies where an entry in opposite direction + /// should automatically close the current position. + /// + /// # Arguments + /// * `entries` - Entry signals + /// * `direction` - Current position direction + /// + /// # Returns + /// Modified exit signals that include opposite-direction entries as exits + pub fn exits_from_opposite_entries( + &self, + long_entries: &[bool], + short_entries: &[bool], + ) -> (Vec, Vec) { + let n = long_entries.len(); + assert_eq!(n, short_entries.len()); + + // Long exits when short entry + // Short exits when long entry + (short_entries.to_vec(), long_entries.to_vec()) + } + + /// Count the number of trades that would be generated from signals. + /// + /// # Arguments + /// * `entries` - Entry signals (already cleaned) + /// * `exits` - Exit signals (already cleaned) + /// + /// # Returns + /// Number of complete trades (entry + exit pairs) + pub fn count_trades(_entries: &[bool], exits: &[bool]) -> usize { + exits.iter().filter(|&&e| e).count() + } + + /// Get indices of entries and exits. + /// + /// # Arguments + /// * `entries` - Entry signals + /// * `exits` - Exit signals + /// + /// # Returns + /// Tuple of (entry_indices, exit_indices) + pub fn get_trade_indices(entries: &[bool], exits: &[bool]) -> (Vec, Vec) { + let entry_indices: Vec = entries + .iter() + .enumerate() + .filter_map(|(i, &e)| if e { Some(i) } else { None }) + .collect(); + + let exit_indices: Vec = exits + .iter() + .enumerate() + .filter_map(|(i, &e)| if e { Some(i) } else { None }) + .collect(); + + (entry_indices, exit_indices) + } +} + +/// Shift signals forward by n bars (delays execution). +pub fn shift_signals(signals: &[bool], n: usize) -> Vec { + let len = signals.len(); + let mut result = vec![false; len]; + + if n >= len { + return result; + } + + for i in n..len { + result[i] = signals[i - n]; + } + + result +} + +/// Combine multiple signal arrays with AND logic. +pub fn combine_signals_and(signals: &[&[bool]]) -> Vec { + if signals.is_empty() { + return vec![]; + } + + let n = signals[0].len(); + for sig in signals.iter() { + assert_eq!(sig.len(), n, "All signal arrays must have same length"); + } + + let mut result = vec![true; n]; + for sig in signals.iter() { + for i in 0..n { + result[i] = result[i] && sig[i]; + } + } + + result +} + +/// Combine multiple signal arrays with OR logic. +pub fn combine_signals_or(signals: &[&[bool]]) -> Vec { + if signals.is_empty() { + return vec![]; + } + + let n = signals[0].len(); + for sig in signals.iter() { + assert_eq!(sig.len(), n, "All signal arrays must have same length"); + } + + let mut result = vec![false; n]; + for sig in signals.iter() { + for i in 0..n { + result[i] = result[i] || sig[i]; + } + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_clean_signals_basic() { + let processor = SignalProcessor::new(); + + let entries = vec![true, false, true, false, true, false]; + let exits = vec![false, true, false, true, false, true]; + + let (clean_e, clean_x) = processor.clean_signals(&entries, &exits); + + // First entry should be kept + assert!(clean_e[0]); + // First exit should be kept + assert!(clean_x[1]); + // Second entry should be kept + assert!(clean_e[2]); + // Second exit should be kept + assert!(clean_x[3]); + } + + #[test] + fn test_clean_signals_consecutive_entries() { + let processor = SignalProcessor::new(); + + let entries = vec![true, true, true, false, false]; + let exits = vec![false, false, false, true, false]; + + let (clean_e, clean_x) = processor.clean_signals(&entries, &exits); + + // Only first entry should be kept + assert!(clean_e[0]); + assert!(!clean_e[1]); + assert!(!clean_e[2]); + // Exit should be kept + assert!(clean_x[3]); + } + + #[test] + fn test_clean_signals_consecutive_exits() { + let processor = SignalProcessor::new(); + + let entries = vec![true, false, false, false, false]; + let exits = vec![false, true, true, true, false]; + + let (clean_e, clean_x) = processor.clean_signals(&entries, &exits); + + // Entry should be kept + assert!(clean_e[0]); + // Only first exit should be kept + assert!(clean_x[1]); + assert!(!clean_x[2]); + assert!(!clean_x[3]); + } + + #[test] + fn test_clean_signals_exit_before_entry() { + let processor = SignalProcessor::new(); + + let entries = vec![false, false, true, false, false]; + let exits = vec![true, true, false, true, false]; + + let (clean_e, clean_x) = processor.clean_signals(&entries, &exits); + + // Exits before first entry should be ignored + assert!(!clean_x[0]); + assert!(!clean_x[1]); + // Entry should be kept + assert!(clean_e[2]); + // Exit after entry should be kept + assert!(clean_x[3]); + } + + #[test] + fn test_pyramiding() { + let processor = SignalProcessor::new().with_pyramiding(3); + + let entries = vec![true, true, true, false, false]; + let exits = vec![false, false, false, true, true]; + + let (clean_e, clean_x) = processor.clean_signals(&entries, &exits); + + // All three entries should be kept (pyramiding) + assert!(clean_e[0]); + assert!(clean_e[1]); + assert!(clean_e[2]); + // Both exits should be kept + assert!(clean_x[3]); + assert!(clean_x[4]); + } + + #[test] + fn test_shift_signals() { + let signals = vec![true, false, true, false, true]; + let shifted = shift_signals(&signals, 2); + + assert!(!shifted[0]); + assert!(!shifted[1]); + assert!(shifted[2]); // Original [0] + assert!(!shifted[3]); // Original [1] + assert!(shifted[4]); // Original [2] + } + + #[test] + fn test_combine_signals_and() { + let sig1 = vec![true, true, false, false]; + let sig2 = vec![true, false, true, false]; + + let combined = combine_signals_and(&[&sig1, &sig2]); + + assert!(combined[0]); // true && true + assert!(!combined[1]); // true && false + assert!(!combined[2]); // false && true + assert!(!combined[3]); // false && false + } + + #[test] + fn test_combine_signals_or() { + let sig1 = vec![true, true, false, false]; + let sig2 = vec![true, false, true, false]; + + let combined = combine_signals_or(&[&sig1, &sig2]); + + assert!(combined[0]); // true || true + assert!(combined[1]); // true || false + assert!(combined[2]); // false || true + assert!(!combined[3]); // false || false + } +} diff --git a/src/signals/synchronizer.rs b/src/signals/synchronizer.rs new file mode 100644 index 0000000..a843e1c --- /dev/null +++ b/src/signals/synchronizer.rs @@ -0,0 +1,399 @@ +//! Signal synchronization for multi-instrument strategies. +//! +//! Handles combining signals from multiple instruments with different sync modes. + +use crate::core::types::CompiledSignals; + +/// Synchronization mode for combining signals from multiple instruments. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SyncMode { + /// All instruments must signal (AND logic). + All, + /// Any instrument can signal (OR logic). + Any, + /// Majority of instruments must signal. + Majority, + /// Use first instrument's signals as master. + Master, +} + +impl Default for SyncMode { + fn default() -> Self { + SyncMode::All + } +} + +/// Signal synchronizer for multi-instrument backtests. +#[derive(Debug, Clone)] +pub struct SignalSynchronizer { + /// Synchronization mode. + pub mode: SyncMode, + /// Minimum number of instruments that must signal (for custom thresholds). + pub min_signals: Option, +} + +impl Default for SignalSynchronizer { + fn default() -> Self { + Self { + mode: SyncMode::All, + min_signals: None, + } + } +} + +impl SignalSynchronizer { + /// Create a new signal synchronizer with the given mode. + pub fn new(mode: SyncMode) -> Self { + Self { + mode, + min_signals: None, + } + } + + /// Create a synchronizer with a custom minimum signal threshold. + pub fn with_min_signals(min: usize) -> Self { + Self { + mode: SyncMode::Majority, + min_signals: Some(min), + } + } + + /// Synchronize entry signals from multiple instruments. + /// + /// # Arguments + /// * `signals` - Slice of signal arrays from each instrument + /// + /// # Returns + /// Combined entry signals based on sync mode + pub fn sync_entries(&self, signals: &[&[bool]]) -> Vec { + if signals.is_empty() { + return vec![]; + } + + let n = signals[0].len(); + for sig in signals.iter() { + assert_eq!(sig.len(), n, "All signal arrays must have same length"); + } + + let num_instruments = signals.len(); + let mut result = vec![false; n]; + + for i in 0..n { + let count = signals.iter().filter(|s| s[i]).count(); + + result[i] = match self.mode { + SyncMode::All => count == num_instruments, + SyncMode::Any => count > 0, + SyncMode::Majority => { + let threshold = self.min_signals.unwrap_or((num_instruments + 1) / 2); + count >= threshold + } + SyncMode::Master => signals[0][i], + }; + } + + result + } + + /// Synchronize exit signals from multiple instruments. + /// + /// Exit logic is typically inverse of entry: + /// - All mode -> exit on Any + /// - Any mode -> exit on All + /// - Majority mode -> exit when majority want to exit + /// - Master mode -> use master's exit signals + /// + /// # Arguments + /// * `signals` - Slice of signal arrays from each instrument + /// + /// # Returns + /// Combined exit signals based on sync mode + pub fn sync_exits(&self, signals: &[&[bool]]) -> Vec { + if signals.is_empty() { + return vec![]; + } + + let n = signals[0].len(); + for sig in signals.iter() { + assert_eq!(sig.len(), n, "All signal arrays must have same length"); + } + + let num_instruments = signals.len(); + let mut result = vec![false; n]; + + for i in 0..n { + let count = signals.iter().filter(|s| s[i]).count(); + + result[i] = match self.mode { + // For All entry mode, exit when ANY wants to exit + SyncMode::All => count > 0, + // For Any entry mode, exit when ALL want to exit + SyncMode::Any => count == num_instruments, + SyncMode::Majority => { + let threshold = self.min_signals.unwrap_or((num_instruments + 1) / 2); + count >= threshold + } + SyncMode::Master => signals[0][i], + }; + } + + result + } + + /// Synchronize signals from CompiledSignals objects. + /// + /// # Arguments + /// * `compiled_signals` - Slice of CompiledSignals from each instrument + /// + /// # Returns + /// Tuple of (synchronized_entries, synchronized_exits) + pub fn sync_compiled_signals( + &self, + compiled_signals: &[&CompiledSignals], + ) -> (Vec, Vec) { + if compiled_signals.is_empty() { + return (vec![], vec![]); + } + + let entries: Vec<&[bool]> = compiled_signals + .iter() + .map(|cs| cs.entries.as_slice()) + .collect(); + + let exits: Vec<&[bool]> = compiled_signals + .iter() + .map(|cs| cs.exits.as_slice()) + .collect(); + + let synced_entries = self.sync_entries(&entries); + let synced_exits = self.sync_exits(&exits); + + (synced_entries, synced_exits) + } + + /// Calculate signal agreement score (0.0 to 1.0). + /// + /// # Arguments + /// * `signals` - Slice of signal arrays from each instrument + /// + /// # Returns + /// Vector of agreement scores for each bar + pub fn signal_agreement(&self, signals: &[&[bool]]) -> Vec { + if signals.is_empty() { + return vec![]; + } + + let n = signals[0].len(); + let num_instruments = signals.len() as f64; + + let mut result = vec![0.0; n]; + + for i in 0..n { + let count = signals.iter().filter(|s| s[i]).count() as f64; + result[i] = count / num_instruments; + } + + result + } +} + +/// Align signals to a common time axis. +/// +/// Useful when instruments have different trading hours or missing data. +/// +/// # Arguments +/// * `signals` - Signal array to align +/// * `source_timestamps` - Timestamps of the signal array +/// * `target_timestamps` - Target timestamp grid +/// * `fill_value` - Value to use for missing timestamps +/// +/// # Returns +/// Aligned signal array +pub fn align_signals( + signals: &[bool], + source_timestamps: &[i64], + target_timestamps: &[i64], + fill_value: bool, +) -> Vec { + let n = target_timestamps.len(); + let mut result = vec![fill_value; n]; + + // Create a map of source timestamps to indices + let mut source_map = std::collections::HashMap::new(); + for (i, &ts) in source_timestamps.iter().enumerate() { + source_map.insert(ts, i); + } + + // Fill in values where timestamps match + for (i, &ts) in target_timestamps.iter().enumerate() { + if let Some(&source_idx) = source_map.get(&ts) { + result[i] = signals[source_idx]; + } + } + + result +} + +/// Forward-fill signals (carry forward last signal). +pub fn forward_fill_signals(signals: &[bool]) -> Vec { + let mut result = signals.to_vec(); + let mut last_value = false; + + for i in 0..result.len() { + if result[i] { + last_value = true; + } + result[i] = last_value; + } + + result +} + +/// Create synchronized position signals. +/// +/// Returns a position signal where: +/// - 1 = in position +/// - 0 = out of position +/// +/// # Arguments +/// * `entries` - Entry signals (cleaned) +/// * `exits` - Exit signals (cleaned) +/// +/// # Returns +/// Position state array +pub fn position_signals(entries: &[bool], exits: &[bool]) -> Vec { + let n = entries.len(); + assert_eq!(n, exits.len()); + + let mut result = vec![0i8; n]; + let mut in_position = false; + + for i in 0..n { + if entries[i] { + in_position = true; + } + if exits[i] { + in_position = false; + } + result[i] = if in_position { 1 } else { 0 }; + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sync_all() { + let sync = SignalSynchronizer::new(SyncMode::All); + + let sig1 = vec![true, true, false, true]; + let sig2 = vec![true, false, false, true]; + let sig3 = vec![true, true, false, true]; + + let result = sync.sync_entries(&[&sig1, &sig2, &sig3]); + + assert!(result[0]); // All true + assert!(!result[1]); // Not all true + assert!(!result[2]); // All false + assert!(result[3]); // All true + } + + #[test] + fn test_sync_any() { + let sync = SignalSynchronizer::new(SyncMode::Any); + + let sig1 = vec![true, false, false, false]; + let sig2 = vec![false, true, false, false]; + let sig3 = vec![false, false, false, false]; + + let result = sync.sync_entries(&[&sig1, &sig2, &sig3]); + + assert!(result[0]); // At least one true + assert!(result[1]); // At least one true + assert!(!result[2]); // All false + assert!(!result[3]); // All false + } + + #[test] + fn test_sync_majority() { + let sync = SignalSynchronizer::new(SyncMode::Majority); + + let sig1 = vec![true, true, false, true]; + let sig2 = vec![true, false, false, true]; + let sig3 = vec![false, true, false, false]; + + let result = sync.sync_entries(&[&sig1, &sig2, &sig3]); + + assert!(result[0]); // 2 out of 3 + assert!(result[1]); // 2 out of 3 + assert!(!result[2]); // 0 out of 3 + assert!(result[3]); // 2 out of 3 + } + + #[test] + fn test_sync_master() { + let sync = SignalSynchronizer::new(SyncMode::Master); + + let sig1 = vec![true, false, true, false]; // Master + let sig2 = vec![false, true, false, true]; + let sig3 = vec![true, true, true, true]; + + let result = sync.sync_entries(&[&sig1, &sig2, &sig3]); + + // Should follow master (sig1) + assert!(result[0]); + assert!(!result[1]); + assert!(result[2]); + assert!(!result[3]); + } + + #[test] + fn test_exit_inverse_logic() { + // For All entry mode, exit should be Any + let sync = SignalSynchronizer::new(SyncMode::All); + + let exit1 = vec![true, false, false]; + let exit2 = vec![false, false, false]; + let exit3 = vec![false, false, false]; + + let result = sync.sync_exits(&[&exit1, &exit2, &exit3]); + + assert!(result[0]); // Any true -> exit + assert!(!result[1]); + assert!(!result[2]); + } + + #[test] + fn test_signal_agreement() { + let sync = SignalSynchronizer::new(SyncMode::All); + + let sig1 = vec![true, true, false, true]; + let sig2 = vec![true, false, false, true]; + let sig3 = vec![false, true, false, true]; + + let result = sync.signal_agreement(&[&sig1, &sig2, &sig3]); + + assert!((result[0] - 2.0 / 3.0).abs() < 1e-10); + assert!((result[1] - 2.0 / 3.0).abs() < 1e-10); + assert!((result[2] - 0.0).abs() < 1e-10); + assert!((result[3] - 1.0).abs() < 1e-10); + } + + #[test] + fn test_position_signals() { + let entries = vec![false, true, false, false, true, false]; + let exits = vec![false, false, false, true, false, true]; + + let result = position_signals(&entries, &exits); + + assert_eq!(result[0], 0); + assert_eq!(result[1], 1); + assert_eq!(result[2], 1); + assert_eq!(result[3], 0); + assert_eq!(result[4], 1); + assert_eq!(result[5], 0); + } +} diff --git a/src/stops/atr.rs b/src/stops/atr.rs new file mode 100644 index 0000000..71391b5 --- /dev/null +++ b/src/stops/atr.rs @@ -0,0 +1,242 @@ +//! ATR-based stop-loss and take-profit. + +use super::{StopCalculator, TargetCalculator}; +use crate::core::types::{Direction, Price}; + +/// ATR-based stop-loss. +#[derive(Debug, Clone)] +pub struct AtrStop { + /// ATR multiplier. + pub multiplier: f64, + /// Current ATR value. + pub atr: f64, +} + +impl AtrStop { + /// Create a new ATR stop. + pub fn new(multiplier: f64, atr: f64) -> Self { + Self { multiplier, atr } + } + + /// Update ATR value. + pub fn update_atr(&mut self, atr: f64) { + self.atr = atr; + } +} + +impl StopCalculator for AtrStop { + fn calculate_stop(&self, entry_price: Price, direction: Direction) -> Option { + if self.atr <= 0.0 { + return None; + } + + let distance = self.atr * self.multiplier; + let stop = match direction { + Direction::Long => entry_price - distance, + Direction::Short => entry_price + distance, + }; + Some(stop) + } + + fn update_stop( + &self, + current_stop: Option, + _current_price: Price, + _high: Price, + _low: Price, + _direction: Direction, + ) -> Option { + // ATR stop doesn't trail by default + current_stop + } +} + +/// ATR-based take-profit. +#[derive(Debug, Clone)] +pub struct AtrTarget { + /// ATR multiplier. + pub multiplier: f64, + /// Current ATR value. + pub atr: f64, +} + +impl AtrTarget { + /// Create a new ATR target. + pub fn new(multiplier: f64, atr: f64) -> Self { + Self { multiplier, atr } + } + + /// Update ATR value. + pub fn update_atr(&mut self, atr: f64) { + self.atr = atr; + } +} + +impl TargetCalculator for AtrTarget { + fn calculate_target( + &self, + entry_price: Price, + _stop_price: Option, + direction: Direction, + ) -> Option { + if self.atr <= 0.0 { + return None; + } + + let distance = self.atr * self.multiplier; + let target = match direction { + Direction::Long => entry_price + distance, + Direction::Short => entry_price - distance, + }; + Some(target) + } +} + +/// Chandelier exit (ATR-based trailing stop from high/low). +#[derive(Debug, Clone)] +pub struct ChandelierExit { + /// ATR multiplier. + pub multiplier: f64, + /// Current ATR value. + pub atr: f64, + /// Highest high since entry (for long). + pub highest_high: f64, + /// Lowest low since entry (for short). + pub lowest_low: f64, +} + +impl ChandelierExit { + /// Create a new Chandelier exit. + pub fn new(multiplier: f64, atr: f64) -> Self { + Self { + multiplier, + atr, + highest_high: 0.0, + lowest_low: f64::MAX, + } + } + + /// Reset for new position. + pub fn reset(&mut self, entry_price: Price) { + self.highest_high = entry_price; + self.lowest_low = entry_price; + } + + /// Update with new bar data. + pub fn update(&mut self, high: Price, low: Price, atr: f64) { + if high > self.highest_high { + self.highest_high = high; + } + if low < self.lowest_low { + self.lowest_low = low; + } + self.atr = atr; + } + + /// Get current stop level. + pub fn stop_level(&self, direction: Direction) -> Option { + if self.atr <= 0.0 { + return None; + } + + let distance = self.atr * self.multiplier; + let stop = match direction { + Direction::Long => self.highest_high - distance, + Direction::Short => self.lowest_low + distance, + }; + Some(stop) + } +} + +impl StopCalculator for ChandelierExit { + fn calculate_stop(&self, entry_price: Price, direction: Direction) -> Option { + if self.atr <= 0.0 { + return None; + } + + let distance = self.atr * self.multiplier; + let stop = match direction { + Direction::Long => entry_price - distance, + Direction::Short => entry_price + distance, + }; + Some(stop) + } + + fn update_stop( + &self, + current_stop: Option, + _current_price: Price, + high: Price, + low: Price, + direction: Direction, + ) -> Option { + if self.atr <= 0.0 { + return current_stop; + } + + let distance = self.atr * self.multiplier; + let new_stop = match direction { + Direction::Long => { + let proposed = high - distance; + current_stop.map(|cs| cs.max(proposed)).or(Some(proposed)) + } + Direction::Short => { + let proposed = low + distance; + current_stop.map(|cs| cs.min(proposed)).or(Some(proposed)) + } + }; + + new_stop + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_atr_stop_long() { + let stop = AtrStop::new(2.0, 5.0); + let result = stop.calculate_stop(100.0, Direction::Long); + // 100 - (2 * 5) = 90 + assert!((result.unwrap() - 90.0).abs() < 1e-10); + } + + #[test] + fn test_atr_stop_short() { + let stop = AtrStop::new(2.0, 5.0); + let result = stop.calculate_stop(100.0, Direction::Short); + // 100 + (2 * 5) = 110 + assert!((result.unwrap() - 110.0).abs() < 1e-10); + } + + #[test] + fn test_atr_target() { + let target = AtrTarget::new(3.0, 5.0); + let result = target.calculate_target(100.0, None, Direction::Long); + // 100 + (3 * 5) = 115 + assert!((result.unwrap() - 115.0).abs() < 1e-10); + } + + #[test] + fn test_chandelier_exit() { + let mut chandelier = ChandelierExit::new(3.0, 2.0); + chandelier.reset(100.0); + + // Simulate price movement up + chandelier.update(105.0, 99.0, 2.0); + chandelier.update(110.0, 103.0, 2.0); + + // Long stop should trail from highest high + // 110 - (3 * 2) = 104 + let stop = chandelier.stop_level(Direction::Long); + assert!((stop.unwrap() - 104.0).abs() < 1e-10); + } + + #[test] + fn test_atr_zero() { + let stop = AtrStop::new(2.0, 0.0); + let result = stop.calculate_stop(100.0, Direction::Long); + assert!(result.is_none()); + } +} diff --git a/src/stops/fixed.rs b/src/stops/fixed.rs new file mode 100644 index 0000000..3718478 --- /dev/null +++ b/src/stops/fixed.rs @@ -0,0 +1,172 @@ +//! Fixed percentage stop-loss and take-profit. + +use super::{StopCalculator, TargetCalculator}; +use crate::core::types::{Direction, Price}; + +/// Fixed percentage stop-loss. +#[derive(Debug, Clone, Copy)] +pub struct FixedStop { + /// Stop percentage (e.g., 0.02 for 2%). + pub percent: f64, +} + +impl FixedStop { + /// Create a new fixed stop with given percentage. + pub fn new(percent: f64) -> Self { + Self { + percent: percent.abs(), + } + } + + /// Create a 1% stop. + pub fn one_percent() -> Self { + Self::new(0.01) + } + + /// Create a 2% stop. + pub fn two_percent() -> Self { + Self::new(0.02) + } + + /// Create a 5% stop. + pub fn five_percent() -> Self { + Self::new(0.05) + } +} + +impl StopCalculator for FixedStop { + fn calculate_stop(&self, entry_price: Price, direction: Direction) -> Option { + let stop = match direction { + Direction::Long => entry_price * (1.0 - self.percent), + Direction::Short => entry_price * (1.0 + self.percent), + }; + Some(stop) + } + + fn update_stop( + &self, + current_stop: Option, + _current_price: Price, + _high: Price, + _low: Price, + _direction: Direction, + ) -> Option { + // Fixed stop doesn't update + current_stop + } +} + +/// Fixed percentage take-profit. +#[derive(Debug, Clone, Copy)] +pub struct FixedTarget { + /// Target percentage (e.g., 0.04 for 4%). + pub percent: f64, +} + +impl FixedTarget { + /// Create a new fixed target with given percentage. + pub fn new(percent: f64) -> Self { + Self { + percent: percent.abs(), + } + } +} + +impl TargetCalculator for FixedTarget { + fn calculate_target( + &self, + entry_price: Price, + _stop_price: Option, + direction: Direction, + ) -> Option { + let target = match direction { + Direction::Long => entry_price * (1.0 + self.percent), + Direction::Short => entry_price * (1.0 - self.percent), + }; + Some(target) + } +} + +/// Risk-reward based take-profit. +#[derive(Debug, Clone, Copy)] +pub struct RiskRewardTarget { + /// Risk-reward ratio (e.g., 2.0 for 2:1 reward:risk). + pub ratio: f64, +} + +impl RiskRewardTarget { + /// Create a new risk-reward target. + pub fn new(ratio: f64) -> Self { + Self { ratio } + } + + /// Create a 2:1 target. + pub fn two_to_one() -> Self { + Self::new(2.0) + } + + /// Create a 3:1 target. + pub fn three_to_one() -> Self { + Self::new(3.0) + } +} + +impl TargetCalculator for RiskRewardTarget { + fn calculate_target( + &self, + entry_price: Price, + stop_price: Option, + direction: Direction, + ) -> Option { + let stop = stop_price?; + let risk = (entry_price - stop).abs(); + let reward = risk * self.ratio; + + let target = match direction { + Direction::Long => entry_price + reward, + Direction::Short => entry_price - reward, + }; + Some(target) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fixed_stop_long() { + let stop = FixedStop::new(0.02); + let result = stop.calculate_stop(100.0, Direction::Long); + assert!((result.unwrap() - 98.0).abs() < 1e-10); + } + + #[test] + fn test_fixed_stop_short() { + let stop = FixedStop::new(0.02); + let result = stop.calculate_stop(100.0, Direction::Short); + assert!((result.unwrap() - 102.0).abs() < 1e-10); + } + + #[test] + fn test_fixed_target_long() { + let target = FixedTarget::new(0.04); + let result = target.calculate_target(100.0, None, Direction::Long); + assert!((result.unwrap() - 104.0).abs() < 1e-10); + } + + #[test] + fn test_risk_reward_target() { + let target = RiskRewardTarget::new(2.0); + // Entry at 100, stop at 98 (2% risk), target should be at 104 (4% reward) + let result = target.calculate_target(100.0, Some(98.0), Direction::Long); + assert!((result.unwrap() - 104.0).abs() < 1e-10); + } + + #[test] + fn test_risk_reward_no_stop() { + let target = RiskRewardTarget::new(2.0); + let result = target.calculate_target(100.0, None, Direction::Long); + assert!(result.is_none()); + } +} diff --git a/src/stops/mod.rs b/src/stops/mod.rs new file mode 100644 index 0000000..1baa81d --- /dev/null +++ b/src/stops/mod.rs @@ -0,0 +1,38 @@ +//! Stop-loss and take-profit mechanisms for RaptorBT. + +pub mod atr; +pub mod fixed; +pub mod trailing; + +pub use atr::AtrStop; +pub use fixed::FixedStop; +pub use trailing::TrailingStop; + +use crate::core::types::{Direction, Price}; + +/// Stop-loss calculator trait. +pub trait StopCalculator { + /// Calculate stop price for a new position. + fn calculate_stop(&self, entry_price: Price, direction: Direction) -> Option; + + /// Update stop price for trailing stops. + fn update_stop( + &self, + current_stop: Option, + current_price: Price, + high: Price, + low: Price, + direction: Direction, + ) -> Option; +} + +/// Take-profit calculator trait. +pub trait TargetCalculator { + /// Calculate target price for a new position. + fn calculate_target( + &self, + entry_price: Price, + stop_price: Option, + direction: Direction, + ) -> Option; +} diff --git a/src/stops/trailing.rs b/src/stops/trailing.rs new file mode 100644 index 0000000..e781272 --- /dev/null +++ b/src/stops/trailing.rs @@ -0,0 +1,403 @@ +//! Trailing stop implementations. + +use super::StopCalculator; +use crate::core::types::{Direction, Price}; + +/// Percentage-based trailing stop. +#[derive(Debug, Clone, Copy)] +pub struct TrailingStop { + /// Trail percentage (e.g., 0.05 for 5%). + pub percent: f64, + /// Activation threshold (optional - start trailing after this profit %). + pub activation_threshold: Option, +} + +impl TrailingStop { + /// Create a new trailing stop. + pub fn new(percent: f64) -> Self { + Self { + percent: percent.abs(), + activation_threshold: None, + } + } + + /// Create with activation threshold. + pub fn with_activation(mut self, threshold: f64) -> Self { + self.activation_threshold = Some(threshold.abs()); + self + } + + /// Check if trailing should be activated. + #[allow(dead_code)] + fn should_activate( + &self, + entry_price: Price, + current_price: Price, + direction: Direction, + ) -> bool { + if let Some(threshold) = self.activation_threshold { + let profit_pct = match direction { + Direction::Long => (current_price - entry_price) / entry_price, + Direction::Short => (entry_price - current_price) / entry_price, + }; + profit_pct >= threshold + } else { + true // Always active if no threshold + } + } +} + +impl StopCalculator for TrailingStop { + fn calculate_stop(&self, entry_price: Price, direction: Direction) -> Option { + let stop = match direction { + Direction::Long => entry_price * (1.0 - self.percent), + Direction::Short => entry_price * (1.0 + self.percent), + }; + Some(stop) + } + + fn update_stop( + &self, + current_stop: Option, + _current_price: Price, + high: Price, + low: Price, + direction: Direction, + ) -> Option { + match direction { + Direction::Long => { + // Trail below the high + let new_stop = high * (1.0 - self.percent); + current_stop.map(|cs| cs.max(new_stop)).or(Some(new_stop)) + } + Direction::Short => { + // Trail above the low + let new_stop = low * (1.0 + self.percent); + current_stop.map(|cs| cs.min(new_stop)).or(Some(new_stop)) + } + } + } +} + +/// Point-based trailing stop (fixed point distance). +#[derive(Debug, Clone, Copy)] +pub struct PointTrailingStop { + /// Trail distance in points. + pub points: f64, +} + +impl PointTrailingStop { + /// Create a new point-based trailing stop. + pub fn new(points: f64) -> Self { + Self { + points: points.abs(), + } + } +} + +impl StopCalculator for PointTrailingStop { + fn calculate_stop(&self, entry_price: Price, direction: Direction) -> Option { + let stop = match direction { + Direction::Long => entry_price - self.points, + Direction::Short => entry_price + self.points, + }; + Some(stop) + } + + fn update_stop( + &self, + current_stop: Option, + _current_price: Price, + high: Price, + low: Price, + direction: Direction, + ) -> Option { + match direction { + Direction::Long => { + let new_stop = high - self.points; + current_stop.map(|cs| cs.max(new_stop)).or(Some(new_stop)) + } + Direction::Short => { + let new_stop = low + self.points; + current_stop.map(|cs| cs.min(new_stop)).or(Some(new_stop)) + } + } + } +} + +/// Step trailing stop (moves in discrete steps). +#[derive(Debug, Clone, Copy)] +pub struct StepTrailingStop { + /// Step size percentage. + pub step_percent: f64, + /// Trail percentage from each step. + pub trail_percent: f64, +} + +impl StepTrailingStop { + /// Create a new step trailing stop. + pub fn new(step_percent: f64, trail_percent: f64) -> Self { + Self { + step_percent: step_percent.abs(), + trail_percent: trail_percent.abs(), + } + } + + /// Calculate stop for a given step level. + fn stop_for_step(&self, entry_price: Price, step: usize, direction: Direction) -> Price { + let step_gain = self.step_percent * step as f64; + match direction { + Direction::Long => { + let step_price = entry_price * (1.0 + step_gain); + step_price * (1.0 - self.trail_percent) + } + Direction::Short => { + let step_price = entry_price * (1.0 - step_gain); + step_price * (1.0 + self.trail_percent) + } + } + } + + /// Determine current step level. + #[allow(dead_code)] + fn current_step( + &self, + entry_price: Price, + extreme_price: Price, + direction: Direction, + ) -> usize { + let gain = match direction { + Direction::Long => (extreme_price - entry_price) / entry_price, + Direction::Short => (entry_price - extreme_price) / entry_price, + }; + + if gain <= 0.0 { + return 0; + } + + (gain / self.step_percent).floor() as usize + } +} + +impl StopCalculator for StepTrailingStop { + fn calculate_stop(&self, entry_price: Price, direction: Direction) -> Option { + Some(self.stop_for_step(entry_price, 0, direction)) + } + + fn update_stop( + &self, + current_stop: Option, + _current_price: Price, + high: Price, + low: Price, + direction: Direction, + ) -> Option { + // This is a simplified version - full implementation would need entry price + // For now, just use regular trailing behavior + match direction { + Direction::Long => { + let new_stop = high * (1.0 - self.trail_percent); + current_stop.map(|cs| cs.max(new_stop)).or(Some(new_stop)) + } + Direction::Short => { + let new_stop = low * (1.0 + self.trail_percent); + current_stop.map(|cs| cs.min(new_stop)).or(Some(new_stop)) + } + } + } +} + +/// Parabolic SAR style trailing stop. +#[derive(Debug, Clone)] +pub struct ParabolicStop { + /// Initial acceleration factor. + pub af_start: f64, + /// Acceleration factor increment. + pub af_step: f64, + /// Maximum acceleration factor. + pub af_max: f64, + /// Current acceleration factor. + current_af: f64, + /// Current extreme point. + extreme_point: f64, + /// Current SAR value. + current_sar: f64, +} + +impl ParabolicStop { + /// Create a new Parabolic SAR stop with default parameters. + pub fn new() -> Self { + Self::with_params(0.02, 0.02, 0.2) + } + + /// Create with custom parameters. + pub fn with_params(af_start: f64, af_step: f64, af_max: f64) -> Self { + Self { + af_start, + af_step, + af_max, + current_af: af_start, + extreme_point: 0.0, + current_sar: 0.0, + } + } + + /// Initialize for new position. + pub fn init(&mut self, entry_price: Price, direction: Direction) { + self.current_af = self.af_start; + self.extreme_point = entry_price; + self.current_sar = match direction { + Direction::Long => entry_price * 0.99, // Slightly below entry + Direction::Short => entry_price * 1.01, // Slightly above entry + }; + } + + /// Update SAR with new bar data. + pub fn update_sar(&mut self, high: Price, low: Price, direction: Direction) -> Price { + // Update extreme point + let new_ep = match direction { + Direction::Long => { + if high > self.extreme_point { + self.current_af = (self.current_af + self.af_step).min(self.af_max); + high + } else { + self.extreme_point + } + } + Direction::Short => { + if low < self.extreme_point { + self.current_af = (self.current_af + self.af_step).min(self.af_max); + low + } else { + self.extreme_point + } + } + }; + self.extreme_point = new_ep; + + // Calculate new SAR + let new_sar = self.current_sar + self.current_af * (self.extreme_point - self.current_sar); + + // Ensure SAR doesn't cross price + self.current_sar = match direction { + Direction::Long => new_sar.min(low), + Direction::Short => new_sar.max(high), + }; + + self.current_sar + } +} + +impl Default for ParabolicStop { + fn default() -> Self { + Self::new() + } +} + +impl StopCalculator for ParabolicStop { + fn calculate_stop(&self, _entry_price: Price, _direction: Direction) -> Option { + if self.current_sar > 0.0 { + Some(self.current_sar) + } else { + None + } + } + + fn update_stop( + &self, + _current_stop: Option, + _current_price: Price, + _high: Price, + _low: Price, + _direction: Direction, + ) -> Option { + // Parabolic stop is updated via update_sar method + if self.current_sar > 0.0 { + Some(self.current_sar) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_trailing_stop_long() { + let stop = TrailingStop::new(0.05); + + // Initial stop + let initial = stop.calculate_stop(100.0, Direction::Long); + assert!((initial.unwrap() - 95.0).abs() < 1e-10); + + // Update with higher high + let updated = stop.update_stop(initial, 108.0, 110.0, 105.0, Direction::Long); + // 110 * 0.95 = 104.5 + assert!((updated.unwrap() - 104.5).abs() < 1e-10); + } + + #[test] + fn test_trailing_stop_short() { + let stop = TrailingStop::new(0.05); + + // Initial stop + let initial = stop.calculate_stop(100.0, Direction::Short); + assert!((initial.unwrap() - 105.0).abs() < 1e-10); + + // Update with lower low + let updated = stop.update_stop(initial, 92.0, 95.0, 90.0, Direction::Short); + // 90 * 1.05 = 94.5 + assert!((updated.unwrap() - 94.5).abs() < 1e-10); + } + + #[test] + fn test_trailing_stop_only_tightens() { + let stop = TrailingStop::new(0.05); + + let initial = stop.calculate_stop(100.0, Direction::Long); + + // Move up + let moved_up = stop.update_stop(initial, 110.0, 110.0, 108.0, Direction::Long); + // 110 * 0.95 = 104.5 + assert!((moved_up.unwrap() - 104.5).abs() < 1e-10); + + // Move down - stop should NOT move down + let moved_down = stop.update_stop(moved_up, 105.0, 106.0, 103.0, Direction::Long); + // Should still be 104.5 (not 106 * 0.95 = 100.7) + assert!((moved_down.unwrap() - 104.5).abs() < 1e-10); + } + + #[test] + fn test_point_trailing_stop() { + let stop = PointTrailingStop::new(5.0); + + // Initial stop + let initial = stop.calculate_stop(100.0, Direction::Long); + assert!((initial.unwrap() - 95.0).abs() < 1e-10); + + // Update with higher high + let updated = stop.update_stop(initial, 108.0, 110.0, 105.0, Direction::Long); + // 110 - 5 = 105 + assert!((updated.unwrap() - 105.0).abs() < 1e-10); + } + + #[test] + fn test_parabolic_stop() { + let mut stop = ParabolicStop::new(); + stop.init(100.0, Direction::Long); + + // Simulate uptrend + let sar1 = stop.update_sar(102.0, 99.0, Direction::Long); + let sar2 = stop.update_sar(105.0, 101.0, Direction::Long); + let sar3 = stop.update_sar(108.0, 103.0, Direction::Long); + + // SAR should be increasing + assert!(sar2 > sar1); + assert!(sar3 > sar2); + + // SAR should be below current low + assert!(sar3 < 103.0); + } +} diff --git a/src/strategies/basket.rs b/src/strategies/basket.rs new file mode 100644 index 0000000..f34a0cb --- /dev/null +++ b/src/strategies/basket.rs @@ -0,0 +1,480 @@ +//! Basket/collective strategy backtest implementation. +//! +//! Supports multiple instruments with synchronized signals. + +use crate::core::types::{ + BacktestConfig, BacktestMetrics, BacktestResult, CompiledSignals, ExitReason, OhlcvData, Trade, +}; +use crate::execution::FeeModel; +use crate::metrics::streaming::StreamingMetrics; +use crate::portfolio::allocation::{AllocationStrategy, CapitalAllocator}; +use crate::signals::processor::SignalProcessor; +use crate::signals::synchronizer::{SignalSynchronizer, SyncMode}; + +/// Basket backtest configuration. +#[derive(Debug, Clone)] +pub struct BasketConfig { + /// Base backtest config. + pub base: BacktestConfig, + /// Signal synchronization mode. + pub sync_mode: SyncMode, + /// Capital allocation strategy. + pub allocation: AllocationStrategy, + /// Whether to rebalance on each signal. + pub rebalance_on_signal: bool, +} + +impl Default for BasketConfig { + fn default() -> Self { + Self { + base: BacktestConfig::default(), + sync_mode: SyncMode::All, + allocation: AllocationStrategy::EqualWeight, + rebalance_on_signal: false, + } + } +} + +/// Basket/collective strategy backtest runner. +#[derive(Debug)] +pub struct BasketBacktest { + /// Configuration. + config: BasketConfig, + /// Signal synchronizer. + synchronizer: SignalSynchronizer, + /// Capital allocator. + #[allow(dead_code)] + allocator: CapitalAllocator, + /// Signal processor. + signal_processor: SignalProcessor, + /// Fee model. + fee_model: FeeModel, +} + +impl BasketBacktest { + /// Create a new basket backtest. + pub fn new(config: BasketConfig) -> Self { + let allocator = CapitalAllocator::new(config.base.initial_capital) + .with_strategy(config.allocation.clone()); + + Self { + synchronizer: SignalSynchronizer::new(config.sync_mode), + allocator, + signal_processor: SignalProcessor::new(), + fee_model: FeeModel::percentage(config.base.fees), + config, + } + } + + /// Run basket backtest with multiple instruments. + /// + /// # Arguments + /// * `instruments` - Vector of (OhlcvData, CompiledSignals) pairs for each instrument + /// + /// # Returns + /// Combined backtest result + pub fn run(&self, instruments: &[(OhlcvData, CompiledSignals)]) -> BacktestResult { + if instruments.is_empty() { + return self.empty_result(); + } + + let n_instruments = instruments.len(); + let n_bars = instruments[0].0.len(); + + // Verify all instruments have same length + for (ohlcv, signals) in instruments { + assert_eq!( + ohlcv.len(), + n_bars, + "All instruments must have same number of bars" + ); + assert_eq!(signals.len(), n_bars, "Signals must match OHLCV length"); + } + + // Synchronize signals + let entry_signals: Vec<&[bool]> = instruments + .iter() + .map(|(_, s)| s.entries.as_slice()) + .collect(); + let exit_signals: Vec<&[bool]> = instruments + .iter() + .map(|(_, s)| s.exits.as_slice()) + .collect(); + + let synced_entries = self.synchronizer.sync_entries(&entry_signals); + let synced_exits = self.synchronizer.sync_exits(&exit_signals); + + // Clean signals + let (clean_entries, clean_exits) = self + .signal_processor + .clean_signals(&synced_entries, &synced_exits); + + // Initialize state + let mut cash = self.config.base.initial_capital; + let mut positions: Vec> = vec![None; n_instruments]; + let mut equity_curve = vec![cash; n_bars]; + let mut drawdown_curve = vec![0.0; n_bars]; + let mut returns = vec![0.0; n_bars]; + let mut trades: Vec = Vec::new(); + let mut streaming = StreamingMetrics::new(); + let mut peak_equity = cash; + let mut trade_counter = 0u64; + + // Main simulation loop + for i in 0..n_bars { + // Calculate current position values + let mut _total_position_value = 0.0; + for (inst_idx, (ohlcv, _)) in instruments.iter().enumerate() { + if let Some(ref pos) = positions[inst_idx] { + _total_position_value += pos.size * ohlcv.close[i]; + } + } + + // Check for exit + if clean_exits[i] { + for (inst_idx, (ohlcv, signals)) in instruments.iter().enumerate() { + if let Some(pos) = positions[inst_idx].take() { + let exit_price = ohlcv.close[i]; + let fees = + self.fee_model + .calculate(exit_price, pos.size, signals.direction); + + let pnl = (exit_price - pos.entry_price) + * pos.size + * signals.direction.multiplier() + - fees; + + let cost_basis = pos.entry_price * pos.size; + let return_pct = if cost_basis > 0.0 { + pnl / cost_basis * 100.0 + } else { + 0.0 + }; + + cash += exit_price * pos.size - fees; + + trades.push(Trade { + id: trade_counter, + symbol: signals.symbol.clone(), + entry_idx: pos.entry_idx, + exit_idx: i, + entry_price: pos.entry_price, + exit_price, + size: pos.size, + direction: signals.direction, + pnl, + return_pct, + entry_time: ohlcv.timestamps[pos.entry_idx], + exit_time: ohlcv.timestamps[i], + fees, + exit_reason: ExitReason::Signal, + }); + + trade_counter += 1; + streaming.update(return_pct / 100.0); + } + } + } + + // Check for entry + if clean_entries[i] && positions.iter().all(|p| p.is_none()) { + // Calculate position sizes + let prices: Vec = instruments.iter().map(|(o, _)| o.close[i]).collect(); + let weights: Vec = instruments.iter().map(|(_, s)| s.weight).collect(); + let sizes = self.calculate_sizes(&prices, &weights, cash); + + // Enter positions + for (inst_idx, (ohlcv, signals)) in instruments.iter().enumerate() { + let size = sizes[inst_idx]; + if size > 0.0 { + let entry_price = ohlcv.close[i]; + let fees = self + .fee_model + .calculate(entry_price, size, signals.direction); + cash -= entry_price * size + fees; + + positions[inst_idx] = Some(PositionState { + entry_idx: i, + entry_price, + size, + }); + } + } + } + + // Update equity + let mut position_value = 0.0; + for (inst_idx, (ohlcv, _)) in instruments.iter().enumerate() { + if let Some(ref pos) = positions[inst_idx] { + position_value += pos.size * ohlcv.close[i]; + } + } + let equity = cash + position_value; + equity_curve[i] = equity; + + // Update drawdown + if equity > peak_equity { + peak_equity = equity; + } + drawdown_curve[i] = (peak_equity - equity) / peak_equity * 100.0; + + // Calculate return + if i > 0 { + returns[i] = (equity - equity_curve[i - 1]) / equity_curve[i - 1]; + } + } + + // Close any remaining positions + let last_idx = n_bars - 1; + for (inst_idx, (ohlcv, signals)) in instruments.iter().enumerate() { + if let Some(pos) = positions[inst_idx].take() { + let exit_price = ohlcv.close[last_idx]; + let fees = self + .fee_model + .calculate(exit_price, pos.size, signals.direction); + + let pnl = + (exit_price - pos.entry_price) * pos.size * signals.direction.multiplier() + - fees; + + let cost_basis = pos.entry_price * pos.size; + let return_pct = if cost_basis > 0.0 { + pnl / cost_basis * 100.0 + } else { + 0.0 + }; + + trades.push(Trade { + id: trade_counter, + symbol: signals.symbol.clone(), + entry_idx: pos.entry_idx, + exit_idx: last_idx, + entry_price: pos.entry_price, + exit_price, + size: pos.size, + direction: signals.direction, + pnl, + return_pct, + entry_time: ohlcv.timestamps[pos.entry_idx], + exit_time: ohlcv.timestamps[last_idx], + fees, + exit_reason: ExitReason::EndOfData, + }); + + trade_counter += 1; + streaming.update(return_pct / 100.0); + } + } + + // Calculate metrics + let metrics = self.calculate_metrics(&equity_curve, &drawdown_curve, &trades, &streaming); + + BacktestResult::new(metrics, equity_curve, drawdown_curve, trades, returns) + } + + /// Calculate position sizes for each instrument. + fn calculate_sizes(&self, prices: &[f64], weights: &[f64], available_capital: f64) -> Vec { + let n = prices.len(); + let total_weight: f64 = weights.iter().sum(); + + if total_weight == 0.0 { + return vec![0.0; n]; + } + + prices + .iter() + .zip(weights.iter()) + .map(|(&price, &weight)| { + if price <= 0.0 { + return 0.0; + } + let allocation = available_capital * (weight / total_weight); + allocation / price + }) + .collect() + } + + /// Calculate metrics for the backtest. + fn calculate_metrics( + &self, + equity_curve: &[f64], + drawdown_curve: &[f64], + trades: &[Trade], + streaming: &StreamingMetrics, + ) -> BacktestMetrics { + let start_value = self.config.base.initial_capital; + let end_value = *equity_curve.last().unwrap_or(&start_value); + + let total_return_pct = (end_value - start_value) / start_value * 100.0; + let max_drawdown_pct = drawdown_curve.iter().fold(0.0f64, |a, &b| a.max(b)); + + let total_trades = trades.len(); + let winning_trades = trades.iter().filter(|t| t.pnl > 0.0).count(); + let losing_trades = trades.iter().filter(|t| t.pnl < 0.0).count(); + + let win_rate_pct = if total_trades > 0 { + winning_trades as f64 / total_trades as f64 * 100.0 + } else { + 0.0 + }; + + let gross_profit: f64 = trades.iter().filter(|t| t.pnl > 0.0).map(|t| t.pnl).sum(); + let gross_loss: f64 = trades + .iter() + .filter(|t| t.pnl < 0.0) + .map(|t| t.pnl.abs()) + .sum(); + let profit_factor = if gross_loss > 0.0 { + gross_profit / gross_loss + } else if gross_profit > 0.0 { + f64::INFINITY + } else { + 0.0 + }; + + let sharpe_ratio = streaming.sharpe_ratio(252.0); + let sortino_ratio = streaming.sortino_ratio(252.0); + let calmar_ratio = if max_drawdown_pct > 0.0 { + total_return_pct / max_drawdown_pct + } else if total_return_pct > 0.0 { + f64::INFINITY + } else { + 0.0 + }; + + BacktestMetrics { + total_return_pct, + sharpe_ratio, + sortino_ratio, + calmar_ratio, + max_drawdown_pct, + win_rate_pct, + profit_factor, + total_trades, + winning_trades, + losing_trades, + start_value, + end_value, + ..Default::default() + } + } + + /// Create empty result. + fn empty_result(&self) -> BacktestResult { + BacktestResult::new( + BacktestMetrics { + start_value: self.config.base.initial_capital, + end_value: self.config.base.initial_capital, + ..Default::default() + }, + vec![], + vec![], + vec![], + vec![], + ) + } +} + +/// Internal position state. +#[derive(Debug, Clone)] +struct PositionState { + entry_idx: usize, + entry_price: f64, + size: f64, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_instruments() -> Vec<(OhlcvData, CompiledSignals)> { + let n = 20; + + let ohlcv1 = OhlcvData { + timestamps: (0..n as i64).collect(), + open: (100..100 + n).map(|x| x as f64).collect(), + high: (101..101 + n).map(|x| x as f64).collect(), + low: (99..99 + n).map(|x| x as f64).collect(), + close: (100..100 + n).map(|x| x as f64 + 0.5).collect(), + volume: vec![1000.0; n], + }; + + let ohlcv2 = OhlcvData { + timestamps: (0..n as i64).collect(), + open: (50..50 + n).map(|x| x as f64).collect(), + high: (51..51 + n).map(|x| x as f64).collect(), + low: (49..49 + n).map(|x| x as f64).collect(), + close: (50..50 + n).map(|x| x as f64 + 0.25).collect(), + volume: vec![2000.0; n], + }; + + let mut entries1 = vec![false; n]; + let mut exits1 = vec![false; n]; + entries1[2] = true; + exits1[8] = true; + + let mut entries2 = vec![false; n]; + let mut exits2 = vec![false; n]; + entries2[2] = true; + exits2[8] = true; + + let signals1 = CompiledSignals { + symbol: "INST1".to_string(), + entries: entries1, + exits: exits1, + position_sizes: None, + direction: Direction::Long, + weight: 1.0, + }; + + let signals2 = CompiledSignals { + symbol: "INST2".to_string(), + entries: entries2, + exits: exits2, + position_sizes: None, + direction: Direction::Long, + weight: 1.0, + }; + + vec![(ohlcv1, signals1), (ohlcv2, signals2)] + } + + #[test] + fn test_basket_backtest() { + let config = BasketConfig::default(); + let backtest = BasketBacktest::new(config); + let instruments = sample_instruments(); + + let result = backtest.run(&instruments); + + // Should have trades for both instruments + assert!(result.trades.len() >= 2); + assert_eq!(result.equity_curve.len(), 20); + } + + #[test] + fn test_sync_mode_all() { + let config = BasketConfig { + sync_mode: SyncMode::All, + ..Default::default() + }; + let backtest = BasketBacktest::new(config); + let instruments = sample_instruments(); + + let result = backtest.run(&instruments); + + // With All mode, both instruments should enter at same time + assert!(result.trades.len() >= 2); + } + + #[test] + fn test_empty_instruments() { + let config = BasketConfig::default(); + let backtest = BasketBacktest::new(config); + + let result = backtest.run(&[]); + + assert_eq!(result.trades.len(), 0); + assert!(result.equity_curve.is_empty()); + } +} diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs new file mode 100644 index 0000000..88e54e9 --- /dev/null +++ b/src/strategies/mod.rs @@ -0,0 +1,13 @@ +//! Strategy implementations for different backtest types. + +pub mod basket; +pub mod multi; +pub mod options; +pub mod pairs; +pub mod single; + +pub use basket::BasketBacktest; +pub use multi::MultiStrategyBacktest; +pub use options::OptionsBacktest; +pub use pairs::PairsBacktest; +pub use single::SingleBacktest; diff --git a/src/strategies/multi.rs b/src/strategies/multi.rs new file mode 100644 index 0000000..89b6bb3 --- /dev/null +++ b/src/strategies/multi.rs @@ -0,0 +1,412 @@ +//! Multi-strategy backtest implementation. +//! +//! Supports running multiple strategies on the same instrument. + +use crate::core::types::{ + BacktestConfig, BacktestMetrics, BacktestResult, CompiledSignals, OhlcvData, Trade, +}; +use crate::execution::FeeModel; +use crate::metrics::streaming::StreamingMetrics; + +/// Strategy combination mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CombineMode { + /// Enter when any strategy signals. + Any, + /// Enter when all strategies signal. + All, + /// Enter when majority of strategies signal. + Majority, + /// Run strategies independently with separate capital. + Independent, + /// Vote-weighted combination. + Weighted, +} + +impl Default for CombineMode { + fn default() -> Self { + CombineMode::Any + } +} + +/// Multi-strategy configuration. +#[derive(Debug, Clone)] +pub struct MultiStrategyConfig { + /// Base backtest config. + pub base: BacktestConfig, + /// Strategy combination mode. + pub combine_mode: CombineMode, + /// Capital allocation per strategy (for independent mode). + pub capital_per_strategy: Option, + /// Strategy weights (for weighted mode). + pub strategy_weights: Vec, +} + +impl Default for MultiStrategyConfig { + fn default() -> Self { + Self { + base: BacktestConfig::default(), + combine_mode: CombineMode::Any, + capital_per_strategy: None, + strategy_weights: vec![], + } + } +} + +/// Multi-strategy backtest runner. +#[derive(Debug)] +pub struct MultiStrategyBacktest { + /// Configuration. + config: MultiStrategyConfig, + /// Fee model. + #[allow(dead_code)] + fee_model: FeeModel, +} + +impl MultiStrategyBacktest { + /// Create a new multi-strategy backtest. + pub fn new(config: MultiStrategyConfig) -> Self { + Self { + fee_model: FeeModel::percentage(config.base.fees), + config, + } + } + + /// Run multi-strategy backtest. + /// + /// # Arguments + /// * `ohlcv` - OHLCV data for the instrument + /// * `strategies` - Vector of compiled signals from each strategy + /// + /// # Returns + /// Combined backtest result + pub fn run(&self, ohlcv: &OhlcvData, strategies: &[CompiledSignals]) -> BacktestResult { + if strategies.is_empty() { + return self.empty_result(); + } + + let n = ohlcv.len(); + for signals in strategies { + assert_eq!( + signals.len(), + n, + "All strategies must have same length as OHLCV" + ); + } + + match self.config.combine_mode { + CombineMode::Independent => self.run_independent(ohlcv, strategies), + _ => self.run_combined(ohlcv, strategies), + } + } + + /// Run strategies independently with separate capital. + fn run_independent(&self, ohlcv: &OhlcvData, strategies: &[CompiledSignals]) -> BacktestResult { + let n_strategies = strategies.len(); + let capital_per = self + .config + .capital_per_strategy + .unwrap_or(self.config.base.initial_capital / n_strategies as f64); + + // Run each strategy independently + let mut all_trades: Vec = Vec::new(); + let mut strategy_equities: Vec> = Vec::new(); + + for (strat_idx, signals) in strategies.iter().enumerate() { + let single_config = BacktestConfig { + initial_capital: capital_per, + ..self.config.base.clone() + }; + let single = crate::strategies::single::SingleBacktest::new(single_config); + let result = single.run(ohlcv, signals); + + // Tag trades with strategy index + for mut trade in result.trades { + trade.symbol = format!("{}_{}", trade.symbol, strat_idx); + all_trades.push(trade); + } + + strategy_equities.push(result.equity_curve); + } + + // Combine equity curves + let n = ohlcv.len(); + let mut combined_equity = vec![0.0; n]; + for i in 0..n { + for equity in &strategy_equities { + combined_equity[i] += equity[i]; + } + } + + // Calculate drawdown + let mut peak = combined_equity[0]; + let mut drawdown_curve = vec![0.0; n]; + for i in 0..n { + if combined_equity[i] > peak { + peak = combined_equity[i]; + } + drawdown_curve[i] = (peak - combined_equity[i]) / peak * 100.0; + } + + // Calculate returns + let mut returns = vec![0.0; n]; + for i in 1..n { + returns[i] = (combined_equity[i] - combined_equity[i - 1]) / combined_equity[i - 1]; + } + + // Calculate metrics + let mut streaming = StreamingMetrics::new(); + for trade in &all_trades { + streaming.update(trade.return_pct / 100.0); + } + + let metrics = self.calculate_metrics( + &combined_equity, + &drawdown_curve, + &all_trades, + &streaming, + self.config.base.initial_capital, + ); + + BacktestResult::new( + metrics, + combined_equity, + drawdown_curve, + all_trades, + returns, + ) + } + + /// Run strategies with combined signals. + fn run_combined(&self, ohlcv: &OhlcvData, strategies: &[CompiledSignals]) -> BacktestResult { + let n = ohlcv.len(); + let n_strategies = strategies.len(); + + // Combine entry signals + let mut combined_entries = vec![false; n]; + let mut combined_exits = vec![false; n]; + + for i in 0..n { + let entry_count = strategies.iter().filter(|s| s.entries[i]).count(); + let exit_count = strategies.iter().filter(|s| s.exits[i]).count(); + + combined_entries[i] = match self.config.combine_mode { + CombineMode::Any => entry_count > 0, + CombineMode::All => entry_count == n_strategies, + CombineMode::Majority => entry_count > n_strategies / 2, + CombineMode::Weighted => { + let weighted_sum: f64 = strategies + .iter() + .enumerate() + .filter(|(_, s)| s.entries[i]) + .map(|(idx, _)| { + self.config + .strategy_weights + .get(idx) + .copied() + .unwrap_or(1.0) + }) + .sum(); + let total_weight: f64 = self + .config + .strategy_weights + .iter() + .sum::() + .max(n_strategies as f64); + weighted_sum / total_weight > 0.5 + } + CombineMode::Independent => unreachable!(), + }; + + // Exit when any strategy wants to exit (conservative) + combined_exits[i] = exit_count > 0; + } + + // Use first strategy's direction and symbol + let direction = strategies[0].direction; + let symbol = strategies[0].symbol.clone(); + + let combined_signals = CompiledSignals { + symbol, + entries: combined_entries, + exits: combined_exits, + position_sizes: None, + direction, + weight: 1.0, + }; + + // Run single backtest with combined signals + let single = crate::strategies::single::SingleBacktest::new(self.config.base.clone()); + single.run(ohlcv, &combined_signals) + } + + /// Calculate metrics. + fn calculate_metrics( + &self, + equity_curve: &[f64], + drawdown_curve: &[f64], + trades: &[Trade], + streaming: &StreamingMetrics, + initial_capital: f64, + ) -> BacktestMetrics { + let start_value = initial_capital; + let end_value = *equity_curve.last().unwrap_or(&start_value); + + let total_return_pct = (end_value - start_value) / start_value * 100.0; + let max_drawdown_pct = drawdown_curve.iter().fold(0.0f64, |a, &b| a.max(b)); + + let total_trades = trades.len(); + let winning_trades = trades.iter().filter(|t| t.pnl > 0.0).count(); + let losing_trades = trades.iter().filter(|t| t.pnl < 0.0).count(); + + let win_rate_pct = if total_trades > 0 { + winning_trades as f64 / total_trades as f64 * 100.0 + } else { + 0.0 + }; + + let gross_profit: f64 = trades.iter().filter(|t| t.pnl > 0.0).map(|t| t.pnl).sum(); + let gross_loss: f64 = trades + .iter() + .filter(|t| t.pnl < 0.0) + .map(|t| t.pnl.abs()) + .sum(); + let profit_factor = if gross_loss > 0.0 { + gross_profit / gross_loss + } else if gross_profit > 0.0 { + f64::INFINITY + } else { + 0.0 + }; + + BacktestMetrics { + total_return_pct, + sharpe_ratio: streaming.sharpe_ratio(252.0), + sortino_ratio: streaming.sortino_ratio(252.0), + calmar_ratio: if max_drawdown_pct > 0.0 { + total_return_pct / max_drawdown_pct + } else { + 0.0 + }, + max_drawdown_pct, + win_rate_pct, + profit_factor, + total_trades, + winning_trades, + losing_trades, + start_value, + end_value, + ..Default::default() + } + } + + /// Create empty result. + fn empty_result(&self) -> BacktestResult { + BacktestResult::new( + BacktestMetrics { + start_value: self.config.base.initial_capital, + end_value: self.config.base.initial_capital, + ..Default::default() + }, + vec![], + vec![], + vec![], + vec![], + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_strategies() -> (OhlcvData, Vec) { + let n = 20; + + let ohlcv = OhlcvData { + timestamps: (0..n as i64).collect(), + open: (100..100 + n).map(|x| x as f64).collect(), + high: (101..101 + n).map(|x| x as f64).collect(), + low: (99..99 + n).map(|x| x as f64).collect(), + close: (100..100 + n).map(|x| x as f64 + 0.5).collect(), + volume: vec![1000.0; n], + }; + + // Strategy 1: Early entry + let mut entries1 = vec![false; n]; + let mut exits1 = vec![false; n]; + entries1[2] = true; + exits1[8] = true; + + // Strategy 2: Later entry + let mut entries2 = vec![false; n]; + let mut exits2 = vec![false; n]; + entries2[4] = true; + exits2[10] = true; + + let signals1 = CompiledSignals { + symbol: "TEST".to_string(), + entries: entries1, + exits: exits1, + position_sizes: None, + direction: Direction::Long, + weight: 1.0, + }; + + let signals2 = CompiledSignals { + symbol: "TEST".to_string(), + entries: entries2, + exits: exits2, + position_sizes: None, + direction: Direction::Long, + weight: 1.0, + }; + + (ohlcv, vec![signals1, signals2]) + } + + #[test] + fn test_multi_any_mode() { + let config = MultiStrategyConfig { + combine_mode: CombineMode::Any, + ..Default::default() + }; + let backtest = MultiStrategyBacktest::new(config); + let (ohlcv, strategies) = sample_strategies(); + + let result = backtest.run(&ohlcv, &strategies); + + // With Any mode, should enter at index 2 (first strategy) + assert!(!result.trades.is_empty()); + } + + #[test] + fn test_multi_all_mode() { + let config = MultiStrategyConfig { + combine_mode: CombineMode::All, + ..Default::default() + }; + let backtest = MultiStrategyBacktest::new(config); + let (ohlcv, strategies) = sample_strategies(); + + let result = backtest.run(&ohlcv, &strategies); + + // With All mode, should not enter (strategies don't signal at same time) + assert!(result.trades.is_empty() || result.trades.len() < 2); + } + + #[test] + fn test_multi_independent_mode() { + let config = MultiStrategyConfig { + combine_mode: CombineMode::Independent, + ..Default::default() + }; + let backtest = MultiStrategyBacktest::new(config); + let (ohlcv, strategies) = sample_strategies(); + + let result = backtest.run(&ohlcv, &strategies); + + // With Independent mode, should have trades from both strategies + assert!(result.trades.len() >= 2); + } +} diff --git a/src/strategies/options.rs b/src/strategies/options.rs new file mode 100644 index 0000000..6254450 --- /dev/null +++ b/src/strategies/options.rs @@ -0,0 +1,450 @@ +//! Options strategy backtest implementation. +//! +//! Supports dynamic strike selection and options-specific position sizing. + +use crate::core::types::{ + BacktestConfig, BacktestMetrics, BacktestResult, CompiledSignals, ExitReason, OhlcvData, Trade, +}; +use crate::execution::FeeModel; +use crate::metrics::streaming::StreamingMetrics; + +/// Options position type. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OptionType { + Call, + Put, +} + +/// Strike selection mode. +#[derive(Debug, Clone, Copy)] +pub enum StrikeSelection { + /// At-the-money (closest to spot). + Atm, + /// In-the-money by N strikes. + Itm(usize), + /// Out-of-the-money by N strikes. + Otm(usize), + /// Fixed strike offset from ATM in percentage. + PercentOffset(f64), + /// Delta-based selection. + Delta(f64), +} + +impl Default for StrikeSelection { + fn default() -> Self { + StrikeSelection::Atm + } +} + +/// Position size type for options. +#[derive(Debug, Clone, Copy)] +pub enum SizeType { + /// Fixed number of contracts. + Contracts(usize), + /// Percentage of capital. + Percent(f64), + /// Fixed notional value. + Notional(f64), + /// Risk-based (percentage of capital at risk). + RiskPercent(f64), +} + +impl Default for SizeType { + fn default() -> Self { + SizeType::Percent(1.0) + } +} + +/// Options backtest configuration. +#[derive(Debug, Clone)] +pub struct OptionsConfig { + /// Base backtest config. + pub base: BacktestConfig, + /// Option type (call/put). + pub option_type: OptionType, + /// Strike selection mode. + pub strike_selection: StrikeSelection, + /// Position size type. + pub size_type: SizeType, + /// Lot size (contracts per lot). + pub lot_size: usize, + /// Strike interval. + pub strike_interval: f64, + /// Days to expiry preference. + pub target_dte: Option, +} + +impl Default for OptionsConfig { + fn default() -> Self { + Self { + base: BacktestConfig::default(), + option_type: OptionType::Call, + strike_selection: StrikeSelection::Atm, + size_type: SizeType::Percent(1.0), + lot_size: 1, + strike_interval: 50.0, + target_dte: None, + } + } +} + +/// Options backtest runner. +#[derive(Debug)] +pub struct OptionsBacktest { + /// Configuration. + config: OptionsConfig, + /// Fee model. + fee_model: FeeModel, +} + +impl OptionsBacktest { + /// Create a new options backtest. + pub fn new(config: OptionsConfig) -> Self { + Self { + fee_model: FeeModel::percentage(config.base.fees), + config, + } + } + + /// Run options backtest. + /// + /// # Arguments + /// * `spot_ohlcv` - Spot/underlying OHLCV data + /// * `option_prices` - Option premium prices (parallel array) + /// * `signals` - Trading signals + /// + /// # Returns + /// Backtest result + pub fn run( + &self, + spot_ohlcv: &OhlcvData, + option_prices: &[f64], + signals: &CompiledSignals, + ) -> BacktestResult { + let n = spot_ohlcv.len(); + assert_eq!(n, option_prices.len()); + assert_eq!(n, signals.len()); + + // Clean signals + let processor = crate::signals::processor::SignalProcessor::new(); + let (entries, exits) = processor.clean_signals(&signals.entries, &signals.exits); + + // Initialize state + let mut cash = self.config.base.initial_capital; + let mut position: Option = None; + let mut equity_curve = vec![cash; n]; + let mut drawdown_curve = vec![0.0; n]; + let mut returns = vec![0.0; n]; + let mut trades: Vec = Vec::new(); + let mut streaming = StreamingMetrics::new(); + let mut peak_equity = cash; + let mut trade_counter = 0u64; + + // Main simulation loop + for i in 0..n { + let spot_price = spot_ohlcv.close[i]; + let option_price = option_prices[i]; + + // Check for exit + if exits[i] { + if let Some(pos) = position.take() { + let exit_price = option_price; + let fees = self.fee_model.calculate( + exit_price, + pos.contracts as f64, + signals.direction, + ); + + let pnl = self.calculate_pnl(&pos, exit_price) - fees; + let cost_basis = + pos.entry_price * pos.contracts as f64 * self.config.lot_size as f64; + let return_pct = if cost_basis > 0.0 { + pnl / cost_basis * 100.0 + } else { + 0.0 + }; + + cash += exit_price * pos.contracts as f64 * self.config.lot_size as f64 - fees; + + trades.push(Trade { + id: trade_counter, + symbol: signals.symbol.clone(), + entry_idx: pos.entry_idx, + exit_idx: i, + entry_price: pos.entry_price, + exit_price, + size: pos.contracts as f64, + direction: signals.direction, + pnl, + return_pct, + entry_time: spot_ohlcv.timestamps[pos.entry_idx], + exit_time: spot_ohlcv.timestamps[i], + fees, + exit_reason: ExitReason::Signal, + }); + + trade_counter += 1; + streaming.update(return_pct / 100.0); + } + } + + // Check for entry + if entries[i] && position.is_none() { + let strike = self.select_strike(spot_price); + let contracts = self.calculate_contracts(option_price, cash); + + if contracts > 0 { + let entry_cost = option_price * contracts as f64 * self.config.lot_size as f64; + let fees = + self.fee_model + .calculate(option_price, contracts as f64, signals.direction); + + cash -= entry_cost + fees; + + position = Some(OptionsPosition { + entry_idx: i, + entry_price: option_price, + strike, + contracts, + option_type: self.config.option_type, + }); + } + } + + // Update equity + let position_value = if let Some(ref pos) = position { + option_price * pos.contracts as f64 * self.config.lot_size as f64 + } else { + 0.0 + }; + let equity = cash + position_value; + equity_curve[i] = equity; + + // Update drawdown + if equity > peak_equity { + peak_equity = equity; + } + drawdown_curve[i] = (peak_equity - equity) / peak_equity * 100.0; + + // Calculate return + if i > 0 { + returns[i] = (equity - equity_curve[i - 1]) / equity_curve[i - 1]; + } + } + + // Close any remaining position + if let Some(pos) = position.take() { + let last_idx = n - 1; + let exit_price = option_prices[last_idx]; + let fees = + self.fee_model + .calculate(exit_price, pos.contracts as f64, signals.direction); + + let pnl = self.calculate_pnl(&pos, exit_price) - fees; + let cost_basis = pos.entry_price * pos.contracts as f64 * self.config.lot_size as f64; + let return_pct = if cost_basis > 0.0 { + pnl / cost_basis * 100.0 + } else { + 0.0 + }; + + trades.push(Trade { + id: trade_counter, + symbol: signals.symbol.clone(), + entry_idx: pos.entry_idx, + exit_idx: last_idx, + entry_price: pos.entry_price, + exit_price, + size: pos.contracts as f64, + direction: signals.direction, + pnl, + return_pct, + entry_time: spot_ohlcv.timestamps[pos.entry_idx], + exit_time: spot_ohlcv.timestamps[last_idx], + fees, + exit_reason: ExitReason::EndOfData, + }); + + streaming.update(return_pct / 100.0); + } + + // Calculate metrics + let metrics = self.calculate_metrics(&equity_curve, &drawdown_curve, &trades, &streaming); + + BacktestResult::new(metrics, equity_curve, drawdown_curve, trades, returns) + } + + /// Select strike price based on configuration. + fn select_strike(&self, spot_price: f64) -> f64 { + let interval = self.config.strike_interval; + let atm_strike = (spot_price / interval).round() * interval; + + match self.config.strike_selection { + StrikeSelection::Atm => atm_strike, + StrikeSelection::Itm(n) => match self.config.option_type { + OptionType::Call => atm_strike - (n as f64 * interval), + OptionType::Put => atm_strike + (n as f64 * interval), + }, + StrikeSelection::Otm(n) => match self.config.option_type { + OptionType::Call => atm_strike + (n as f64 * interval), + OptionType::Put => atm_strike - (n as f64 * interval), + }, + StrikeSelection::PercentOffset(pct) => { + let offset = spot_price * pct; + match self.config.option_type { + OptionType::Call => atm_strike + offset, + OptionType::Put => atm_strike - offset, + } + } + StrikeSelection::Delta(_) => atm_strike, // Simplified - would need options chain + } + } + + /// Calculate number of contracts based on size type. + fn calculate_contracts(&self, option_price: f64, available_capital: f64) -> usize { + if option_price <= 0.0 { + return 0; + } + + let contract_cost = option_price * self.config.lot_size as f64; + + match self.config.size_type { + SizeType::Contracts(n) => n, + SizeType::Percent(pct) => { + let allocation = available_capital * pct; + (allocation / contract_cost) as usize + } + SizeType::Notional(value) => (value / contract_cost) as usize, + SizeType::RiskPercent(pct) => { + // Max loss is the premium paid + let risk_amount = available_capital * pct; + (risk_amount / contract_cost) as usize + } + } + } + + /// Calculate P&L for a position. + fn calculate_pnl(&self, position: &OptionsPosition, current_price: f64) -> f64 { + let multiplier = self.config.lot_size as f64; + (current_price - position.entry_price) * position.contracts as f64 * multiplier + } + + /// Calculate metrics. + fn calculate_metrics( + &self, + equity_curve: &[f64], + drawdown_curve: &[f64], + trades: &[Trade], + streaming: &StreamingMetrics, + ) -> BacktestMetrics { + let start_value = self.config.base.initial_capital; + let end_value = *equity_curve.last().unwrap_or(&start_value); + + let total_return_pct = (end_value - start_value) / start_value * 100.0; + let max_drawdown_pct = drawdown_curve.iter().fold(0.0f64, |a, &b| a.max(b)); + + let total_trades = trades.len(); + let winning_trades = trades.iter().filter(|t| t.pnl > 0.0).count(); + let losing_trades = trades.iter().filter(|t| t.pnl < 0.0).count(); + + let win_rate_pct = if total_trades > 0 { + winning_trades as f64 / total_trades as f64 * 100.0 + } else { + 0.0 + }; + + let gross_profit: f64 = trades.iter().filter(|t| t.pnl > 0.0).map(|t| t.pnl).sum(); + let gross_loss: f64 = trades + .iter() + .filter(|t| t.pnl < 0.0) + .map(|t| t.pnl.abs()) + .sum(); + let profit_factor = if gross_loss > 0.0 { + gross_profit / gross_loss + } else if gross_profit > 0.0 { + f64::INFINITY + } else { + 0.0 + }; + + BacktestMetrics { + total_return_pct, + sharpe_ratio: streaming.sharpe_ratio(252.0), + sortino_ratio: streaming.sortino_ratio(252.0), + calmar_ratio: if max_drawdown_pct > 0.0 { + total_return_pct / max_drawdown_pct + } else { + 0.0 + }, + max_drawdown_pct, + win_rate_pct, + profit_factor, + total_trades, + winning_trades, + losing_trades, + start_value, + end_value, + ..Default::default() + } + } +} + +/// Internal options position state. +#[derive(Debug, Clone)] +struct OptionsPosition { + entry_idx: usize, + entry_price: f64, + #[allow(dead_code)] + strike: f64, + contracts: usize, + #[allow(dead_code)] + option_type: OptionType, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_strike_selection_atm() { + let config = OptionsConfig { + strike_interval: 50.0, + strike_selection: StrikeSelection::Atm, + ..Default::default() + }; + let backtest = OptionsBacktest::new(config); + + // Spot at 17834, ATM should be 17850 + let strike = backtest.select_strike(17834.0); + assert!((strike - 17850.0).abs() < 1e-10); + } + + #[test] + fn test_strike_selection_otm() { + let config = OptionsConfig { + strike_interval: 50.0, + strike_selection: StrikeSelection::Otm(2), + option_type: OptionType::Call, + ..Default::default() + }; + let backtest = OptionsBacktest::new(config); + + // Spot at 17834, ATM=17850, OTM 2 strikes = 17950 + let strike = backtest.select_strike(17834.0); + assert!((strike - 17950.0).abs() < 1e-10); + } + + #[test] + fn test_position_sizing_percent() { + let config = OptionsConfig { + size_type: SizeType::Percent(0.5), + lot_size: 50, + ..Default::default() + }; + let backtest = OptionsBacktest::new(config); + + // 50% of 100000 = 50000, option at 100 * lot 50 = 5000 per contract + let contracts = backtest.calculate_contracts(100.0, 100_000.0); + assert_eq!(contracts, 10); + } +} diff --git a/src/strategies/pairs.rs b/src/strategies/pairs.rs new file mode 100644 index 0000000..a1e7bea --- /dev/null +++ b/src/strategies/pairs.rs @@ -0,0 +1,481 @@ +//! Pairs trading strategy backtest implementation. +//! +//! Supports long/short legs with hedge ratios. + +use crate::core::types::{ + BacktestConfig, BacktestMetrics, BacktestResult, CompiledSignals, Direction, ExitReason, + OhlcvData, Trade, +}; +use crate::execution::FeeModel; +use crate::metrics::streaming::StreamingMetrics; + +/// Pairs trading configuration. +#[derive(Debug, Clone)] +pub struct PairsConfig { + /// Base backtest config. + pub base: BacktestConfig, + /// Hedge ratio (units of leg2 per unit of leg1). + pub hedge_ratio: f64, + /// Whether to dynamically update hedge ratio. + pub dynamic_hedge: bool, + /// Lookback period for dynamic hedge calculation. + pub hedge_lookback: usize, + /// Maximum spread for entry. + pub max_spread: Option, + /// Entry z-score threshold. + pub entry_zscore: f64, + /// Exit z-score threshold. + pub exit_zscore: f64, +} + +impl Default for PairsConfig { + fn default() -> Self { + Self { + base: BacktestConfig::default(), + hedge_ratio: 1.0, + dynamic_hedge: false, + hedge_lookback: 20, + max_spread: None, + entry_zscore: 2.0, + exit_zscore: 0.5, + } + } +} + +/// Pairs trading backtest runner. +#[derive(Debug)] +pub struct PairsBacktest { + /// Configuration. + config: PairsConfig, + /// Fee model. + fee_model: FeeModel, +} + +impl PairsBacktest { + /// Create a new pairs backtest. + pub fn new(config: PairsConfig) -> Self { + Self { + fee_model: FeeModel::percentage(config.base.fees), + config, + } + } + + /// Run pairs trading backtest. + /// + /// # Arguments + /// * `leg1_ohlcv` - OHLCV data for leg 1 (long leg when spread widens) + /// * `leg2_ohlcv` - OHLCV data for leg 2 (short leg when spread widens) + /// * `signals` - Entry/exit signals based on spread + /// + /// # Returns + /// Backtest result + pub fn run( + &self, + leg1_ohlcv: &OhlcvData, + leg2_ohlcv: &OhlcvData, + signals: &CompiledSignals, + ) -> BacktestResult { + let n = leg1_ohlcv.len(); + assert_eq!(n, leg2_ohlcv.len()); + assert_eq!(n, signals.len()); + + // Clean signals + let processor = crate::signals::processor::SignalProcessor::new(); + let (entries, exits) = processor.clean_signals(&signals.entries, &signals.exits); + + // Initialize state + let mut cash = self.config.base.initial_capital; + let mut position: Option = None; + let mut equity_curve = vec![cash; n]; + let mut drawdown_curve = vec![0.0; n]; + let mut returns = vec![0.0; n]; + let mut trades: Vec = Vec::new(); + let mut streaming = StreamingMetrics::new(); + let mut peak_equity = cash; + let mut trade_counter = 0u64; + + // Main simulation loop + for i in 0..n { + let leg1_price = leg1_ohlcv.close[i]; + let leg2_price = leg2_ohlcv.close[i]; + + // Calculate current hedge ratio + let hedge_ratio = if self.config.dynamic_hedge && i >= self.config.hedge_lookback { + self.calculate_hedge_ratio( + &leg1_ohlcv.close[i - self.config.hedge_lookback..=i], + &leg2_ohlcv.close[i - self.config.hedge_lookback..=i], + ) + } else { + self.config.hedge_ratio + }; + + // Check for exit + if exits[i] { + if let Some(pos) = position.take() { + let (pnl, fees) = self.close_position(&pos, leg1_price, leg2_price); + let cost_basis = pos.leg1_cost + pos.leg2_cost; + let return_pct = if cost_basis > 0.0 { + pnl / cost_basis * 100.0 + } else { + 0.0 + }; + + // Return capital + cash += pos.leg1_size * leg1_price + pos.leg2_size * leg2_price - fees; + + // Record trades for both legs + trades.push(Trade { + id: trade_counter, + symbol: format!("{}_LEG1", signals.symbol), + entry_idx: pos.entry_idx, + exit_idx: i, + entry_price: pos.leg1_entry_price, + exit_price: leg1_price, + size: pos.leg1_size, + direction: pos.leg1_direction, + pnl: pnl / 2.0, // Split P&L attribution + return_pct: return_pct / 2.0, + entry_time: leg1_ohlcv.timestamps[pos.entry_idx], + exit_time: leg1_ohlcv.timestamps[i], + fees: fees / 2.0, + exit_reason: ExitReason::Signal, + }); + + trade_counter += 1; + + trades.push(Trade { + id: trade_counter, + symbol: format!("{}_LEG2", signals.symbol), + entry_idx: pos.entry_idx, + exit_idx: i, + entry_price: pos.leg2_entry_price, + exit_price: leg2_price, + size: pos.leg2_size, + direction: pos.leg2_direction, + pnl: pnl / 2.0, + return_pct: return_pct / 2.0, + entry_time: leg2_ohlcv.timestamps[pos.entry_idx], + exit_time: leg2_ohlcv.timestamps[i], + fees: fees / 2.0, + exit_reason: ExitReason::Signal, + }); + + trade_counter += 1; + streaming.update(return_pct / 100.0); + } + } + + // Check for entry + if entries[i] && position.is_none() { + // Determine direction from signal direction + let (leg1_dir, leg2_dir) = match signals.direction { + Direction::Long => (Direction::Long, Direction::Short), + Direction::Short => (Direction::Short, Direction::Long), + }; + + // Calculate position sizes + let allocation = cash * 0.5; // Use 50% per leg + let leg1_size = allocation / leg1_price; + let leg2_size = (allocation * hedge_ratio) / leg2_price; + + let leg1_cost = leg1_size * leg1_price; + let leg2_cost = leg2_size * leg2_price; + let entry_fees = self.fee_model.calculate(leg1_price, leg1_size, leg1_dir) + + self.fee_model.calculate(leg2_price, leg2_size, leg2_dir); + + cash -= leg1_cost + leg2_cost + entry_fees; + + position = Some(PairsPosition { + entry_idx: i, + leg1_entry_price: leg1_price, + leg2_entry_price: leg2_price, + leg1_size, + leg2_size, + leg1_direction: leg1_dir, + leg2_direction: leg2_dir, + leg1_cost, + leg2_cost, + hedge_ratio, + }); + } + + // Update equity + let position_value = if let Some(ref pos) = position { + let _leg1_value = pos.leg1_size * leg1_price; + let _leg2_value = pos.leg2_size * leg2_price; + + // For pairs, value is long leg - short leg + cash equivalent + let leg1_pnl = (leg1_price - pos.leg1_entry_price) + * pos.leg1_size + * pos.leg1_direction.multiplier(); + let leg2_pnl = (leg2_price - pos.leg2_entry_price) + * pos.leg2_size + * pos.leg2_direction.multiplier(); + + pos.leg1_cost + pos.leg2_cost + leg1_pnl + leg2_pnl + } else { + 0.0 + }; + + let equity = cash + position_value; + equity_curve[i] = equity; + + // Update drawdown + if equity > peak_equity { + peak_equity = equity; + } + drawdown_curve[i] = (peak_equity - equity) / peak_equity * 100.0; + + // Calculate return + if i > 0 { + returns[i] = (equity - equity_curve[i - 1]) / equity_curve[i - 1]; + } + } + + // Close any remaining position + if let Some(pos) = position.take() { + let last_idx = n - 1; + let leg1_price = leg1_ohlcv.close[last_idx]; + let leg2_price = leg2_ohlcv.close[last_idx]; + + let (pnl, fees) = self.close_position(&pos, leg1_price, leg2_price); + let cost_basis = pos.leg1_cost + pos.leg2_cost; + let return_pct = if cost_basis > 0.0 { + pnl / cost_basis * 100.0 + } else { + 0.0 + }; + + trades.push(Trade { + id: trade_counter, + symbol: signals.symbol.clone(), + entry_idx: pos.entry_idx, + exit_idx: last_idx, + entry_price: pos.leg1_entry_price, + exit_price: leg1_price, + size: pos.leg1_size + pos.leg2_size, + direction: pos.leg1_direction, + pnl, + return_pct, + entry_time: leg1_ohlcv.timestamps[pos.entry_idx], + exit_time: leg1_ohlcv.timestamps[last_idx], + fees, + exit_reason: ExitReason::EndOfData, + }); + + streaming.update(return_pct / 100.0); + } + + // Calculate metrics + let metrics = self.calculate_metrics(&equity_curve, &drawdown_curve, &trades, &streaming); + + BacktestResult::new(metrics, equity_curve, drawdown_curve, trades, returns) + } + + /// Calculate hedge ratio using OLS regression. + fn calculate_hedge_ratio(&self, leg1_prices: &[f64], leg2_prices: &[f64]) -> f64 { + let n = leg1_prices.len() as f64; + if n < 2.0 { + return self.config.hedge_ratio; + } + + let sum_x: f64 = leg2_prices.iter().sum(); + let sum_y: f64 = leg1_prices.iter().sum(); + let sum_xy: f64 = leg1_prices + .iter() + .zip(leg2_prices.iter()) + .map(|(y, x)| x * y) + .sum(); + let sum_x2: f64 = leg2_prices.iter().map(|x| x * x).sum(); + + let denominator = n * sum_x2 - sum_x * sum_x; + if denominator.abs() < 1e-10 { + return self.config.hedge_ratio; + } + + let beta = (n * sum_xy - sum_x * sum_y) / denominator; + beta.max(0.1).min(10.0) // Constrain to reasonable range + } + + /// Close position and calculate P&L. + fn close_position( + &self, + position: &PairsPosition, + leg1_price: f64, + leg2_price: f64, + ) -> (f64, f64) { + let leg1_pnl = (leg1_price - position.leg1_entry_price) + * position.leg1_size + * position.leg1_direction.multiplier(); + + let leg2_pnl = (leg2_price - position.leg2_entry_price) + * position.leg2_size + * position.leg2_direction.multiplier(); + + let exit_fees = + self.fee_model + .calculate(leg1_price, position.leg1_size, position.leg1_direction) + + self + .fee_model + .calculate(leg2_price, position.leg2_size, position.leg2_direction); + + let total_pnl = leg1_pnl + leg2_pnl - exit_fees; + + (total_pnl, exit_fees) + } + + /// Calculate metrics. + fn calculate_metrics( + &self, + equity_curve: &[f64], + drawdown_curve: &[f64], + trades: &[Trade], + streaming: &StreamingMetrics, + ) -> BacktestMetrics { + let start_value = self.config.base.initial_capital; + let end_value = *equity_curve.last().unwrap_or(&start_value); + + let total_return_pct = (end_value - start_value) / start_value * 100.0; + let max_drawdown_pct = drawdown_curve.iter().fold(0.0f64, |a, &b| a.max(b)); + + // For pairs, count trade pairs (every 2 trades = 1 round trip) + let total_trades = trades.len() / 2; + let winning_trades = trades + .chunks(2) + .filter(|chunk| chunk.iter().map(|t| t.pnl).sum::() > 0.0) + .count(); + let losing_trades = total_trades.saturating_sub(winning_trades); + + let win_rate_pct = if total_trades > 0 { + winning_trades as f64 / total_trades as f64 * 100.0 + } else { + 0.0 + }; + + let gross_profit: f64 = trades.iter().filter(|t| t.pnl > 0.0).map(|t| t.pnl).sum(); + let gross_loss: f64 = trades + .iter() + .filter(|t| t.pnl < 0.0) + .map(|t| t.pnl.abs()) + .sum(); + let profit_factor = if gross_loss > 0.0 { + gross_profit / gross_loss + } else if gross_profit > 0.0 { + f64::INFINITY + } else { + 0.0 + }; + + BacktestMetrics { + total_return_pct, + sharpe_ratio: streaming.sharpe_ratio(252.0), + sortino_ratio: streaming.sortino_ratio(252.0), + calmar_ratio: if max_drawdown_pct > 0.0 { + total_return_pct / max_drawdown_pct + } else { + 0.0 + }, + max_drawdown_pct, + win_rate_pct, + profit_factor, + total_trades, + winning_trades, + losing_trades, + start_value, + end_value, + ..Default::default() + } + } +} + +/// Internal pairs position state. +#[derive(Debug, Clone)] +struct PairsPosition { + entry_idx: usize, + leg1_entry_price: f64, + leg2_entry_price: f64, + leg1_size: f64, + leg2_size: f64, + leg1_direction: Direction, + leg2_direction: Direction, + leg1_cost: f64, + leg2_cost: f64, + #[allow(dead_code)] + hedge_ratio: f64, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_pairs_data() -> (OhlcvData, OhlcvData, CompiledSignals) { + let n = 20; + + // Leg 1: Trending up + let leg1 = OhlcvData { + timestamps: (0..n as i64).collect(), + open: (100..100 + n).map(|x| x as f64).collect(), + high: (101..101 + n).map(|x| x as f64).collect(), + low: (99..99 + n).map(|x| x as f64).collect(), + close: (100..100 + n).map(|x| x as f64 + 0.5).collect(), + volume: vec![1000.0; n], + }; + + // Leg 2: Correlated but with different magnitude + let leg2 = OhlcvData { + timestamps: (0..n as i64).collect(), + open: (50..50 + n).map(|x| x as f64).collect(), + high: (51..51 + n).map(|x| x as f64).collect(), + low: (49..49 + n).map(|x| x as f64).collect(), + close: (50..50 + n).map(|x| x as f64 + 0.2).collect(), + volume: vec![2000.0; n], + }; + + let mut entries = vec![false; n]; + let mut exits = vec![false; n]; + entries[2] = true; + exits[10] = true; + + let signals = CompiledSignals { + symbol: "PAIR".to_string(), + entries, + exits, + position_sizes: None, + direction: Direction::Long, // Long leg1, short leg2 + weight: 1.0, + }; + + (leg1, leg2, signals) + } + + #[test] + fn test_pairs_backtest() { + let config = PairsConfig::default(); + let backtest = PairsBacktest::new(config); + let (leg1, leg2, signals) = sample_pairs_data(); + + let result = backtest.run(&leg1, &leg2, &signals); + + // Should have trades for both legs + assert!(result.trades.len() >= 2); + assert_eq!(result.equity_curve.len(), 20); + } + + #[test] + fn test_hedge_ratio_calculation() { + let config = PairsConfig { + dynamic_hedge: true, + hedge_lookback: 5, + ..Default::default() + }; + let backtest = PairsBacktest::new(config); + + let leg1 = vec![100.0, 102.0, 104.0, 106.0, 108.0]; + let leg2 = vec![50.0, 51.0, 52.0, 53.0, 54.0]; + + let ratio = backtest.calculate_hedge_ratio(&leg1, &leg2); + + // Ratio should be approximately 2 (leg1 moves 2x leg2) + assert!(ratio > 1.5 && ratio < 2.5); + } +} diff --git a/src/strategies/single.rs b/src/strategies/single.rs new file mode 100644 index 0000000..7a52d61 --- /dev/null +++ b/src/strategies/single.rs @@ -0,0 +1,206 @@ +//! Single instrument backtest implementation. + +use crate::core::types::{BacktestConfig, BacktestResult, CompiledSignals, OhlcvData}; +use crate::portfolio::engine::PortfolioEngine; + +/// Single instrument backtest runner. +#[derive(Debug)] +pub struct SingleBacktest { + /// Portfolio engine. + engine: PortfolioEngine, +} + +impl SingleBacktest { + /// Create a new single instrument backtest. + pub fn new(config: BacktestConfig) -> Self { + Self { + engine: PortfolioEngine::new(config), + } + } + + /// Run the backtest. + /// + /// # Arguments + /// * `ohlcv` - OHLCV price data + /// * `signals` - Compiled trading signals + /// + /// # Returns + /// Backtest result with metrics, trades, and equity curve + pub fn run(&self, ohlcv: &OhlcvData, signals: &CompiledSignals) -> BacktestResult { + self.engine.run_single(ohlcv, signals) + } + + /// Run backtest from raw arrays. + /// + /// # Arguments + /// * `timestamps` - Timestamp array + /// * `open` - Open prices + /// * `high` - High prices + /// * `low` - Low prices + /// * `close` - Close prices + /// * `volume` - Volume + /// * `entries` - Entry signals + /// * `exits` - Exit signals + /// * `direction` - Trade direction (1 = long, -1 = short) + /// * `symbol` - Symbol name + /// + /// # Returns + /// Backtest result + pub fn run_from_arrays( + &self, + timestamps: &[i64], + open: &[f64], + high: &[f64], + low: &[f64], + close: &[f64], + volume: &[f64], + entries: &[bool], + exits: &[bool], + direction: i32, + symbol: &str, + ) -> BacktestResult { + let ohlcv = OhlcvData { + timestamps: timestamps.to_vec(), + open: open.to_vec(), + high: high.to_vec(), + low: low.to_vec(), + close: close.to_vec(), + volume: volume.to_vec(), + }; + + let dir = crate::core::types::Direction::from_int(direction) + .unwrap_or(crate::core::types::Direction::Long); + + let signals = CompiledSignals { + symbol: symbol.to_string(), + entries: entries.to_vec(), + exits: exits.to_vec(), + position_sizes: None, + direction: dir, + weight: 1.0, + }; + + self.run(&ohlcv, &signals) + } + + /// Run backtest with position sizing. + /// + /// # Arguments + /// * `ohlcv` - OHLCV price data + /// * `signals` - Compiled trading signals + /// * `position_sizes` - Position size for each bar (fraction of capital) + /// + /// # Returns + /// Backtest result + pub fn run_with_sizing( + &self, + ohlcv: &OhlcvData, + signals: &CompiledSignals, + position_sizes: Vec, + ) -> BacktestResult { + let mut signals_with_sizing = signals.clone(); + signals_with_sizing.position_sizes = Some(position_sizes); + self.engine.run_single(ohlcv, &signals_with_sizing) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::types::{Direction, StopConfig, TargetConfig}; + + fn sample_data() -> (OhlcvData, CompiledSignals) { + let ohlcv = OhlcvData { + timestamps: (0..20).map(|i| i as i64).collect(), + open: vec![ + 100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 104.0, 103.0, 102.0, 101.0, 100.0, 101.0, + 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 109.0, + ], + high: vec![ + 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 105.0, 104.0, 103.0, 102.0, 101.0, 102.0, + 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 109.0, 110.0, + ], + low: vec![ + 99.0, 100.0, 101.0, 102.0, 103.0, 104.0, 103.0, 102.0, 101.0, 100.0, 99.0, 100.0, + 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, + ], + close: vec![ + 100.5, 101.5, 102.5, 103.5, 104.5, 105.0, 104.0, 103.0, 102.0, 101.0, 100.5, 101.5, + 102.5, 103.5, 104.5, 105.5, 106.5, 107.5, 108.5, 109.5, + ], + volume: vec![1000.0; 20], + }; + + let signals = CompiledSignals { + symbol: "TEST".to_string(), + entries: vec![ + false, true, false, false, false, false, false, false, false, false, false, true, + false, false, false, false, false, false, false, false, + ], + exits: vec![ + false, false, false, false, false, true, false, false, false, false, false, false, + false, false, false, true, false, false, false, false, + ], + position_sizes: None, + direction: Direction::Long, + weight: 1.0, + }; + + (ohlcv, signals) + } + + #[test] + fn test_single_backtest() { + let config = BacktestConfig { + initial_capital: 100_000.0, + fees: 0.0, + slippage: 0.0, + stop: StopConfig::None, + target: TargetConfig::None, + upon_bar_close: true, + }; + + let backtest = SingleBacktest::new(config); + let (ohlcv, signals) = sample_data(); + + let result = backtest.run(&ohlcv, &signals); + + assert_eq!(result.trades.len(), 2); + assert!(result.metrics.total_return_pct > 0.0); + } + + #[test] + fn test_from_arrays() { + let config = BacktestConfig::default(); + let backtest = SingleBacktest::new(config); + + let timestamps: Vec = (0..10).collect(); + let close: Vec = (100..110).map(|x| x as f64).collect(); + let open = close.clone(); + let high: Vec = close.iter().map(|x| x + 1.0).collect(); + let low: Vec = close.iter().map(|x| x - 1.0).collect(); + let volume = vec![1000.0; 10]; + + let entries = vec![ + false, true, false, false, false, false, false, false, false, false, + ]; + let exits = vec![ + false, false, false, false, false, true, false, false, false, false, + ]; + + let result = backtest.run_from_arrays( + ×tamps, + &open, + &high, + &low, + &close, + &volume, + &entries, + &exits, + 1, + "TEST", + ); + + assert_eq!(result.trades.len(), 1); + } +} diff --git a/tests/test_indicators.rs b/tests/test_indicators.rs new file mode 100644 index 0000000..2850a1b --- /dev/null +++ b/tests/test_indicators.rs @@ -0,0 +1,240 @@ +//! Integration tests for RaptorBT indicators. + +use raptorbt::indicators::momentum::{macd, rsi, stochastic}; +use raptorbt::indicators::strength::adx; +use raptorbt::indicators::trend::{ema, sma, supertrend}; +use raptorbt::indicators::volatility::{atr, bollinger_bands}; +use raptorbt::indicators::volume::vwap; + +fn sample_ohlcv() -> (Vec, Vec, Vec, Vec, Vec) { + // Create sample OHLCV data with 50 bars + let n = 50; + let mut close: Vec = vec![100.0]; + let mut high: Vec = vec![101.0]; + let mut low: Vec = vec![99.0]; + let mut open: Vec = vec![100.0]; + let volume: Vec = vec![1000.0; n]; + + // Generate trending data + for i in 1..n { + let prev_close = close[i - 1]; + let change = ((i as f64 * 0.2).sin() * 2.0) + 0.5; // Slight uptrend with oscillation + let new_close = prev_close + change; + close.push(new_close); + open.push(prev_close); + high.push(new_close.max(prev_close) + 0.5); + low.push(new_close.min(prev_close) - 0.5); + } + + (open, high, low, close, volume) +} + +#[test] +fn test_sma_correctness() { + let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]; + let result = sma(&data, 3).unwrap(); + + // First 2 values should be NaN + assert!(result[0].is_nan()); + assert!(result[1].is_nan()); + + // SMA(3) for [1,2,3] = 2.0 + assert!((result[2] - 2.0).abs() < 1e-10); + // SMA(3) for [2,3,4] = 3.0 + assert!((result[3] - 3.0).abs() < 1e-10); + // SMA(3) for [8,9,10] = 9.0 + assert!((result[9] - 9.0).abs() < 1e-10); +} + +#[test] +fn test_ema_correctness() { + let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]; + let result = ema(&data, 3).unwrap(); + + // First 2 values should be NaN + assert!(result[0].is_nan()); + assert!(result[1].is_nan()); + + // EMA should be valid from index 2 + assert!(!result[2].is_nan()); + assert!(!result[9].is_nan()); + + // EMA should be between min and max + assert!(result[9] >= 1.0 && result[9] <= 10.0); +} + +#[test] +fn test_rsi_range() { + let (_, _, _, close, _) = sample_ohlcv(); + let result = rsi(&close, 14).unwrap(); + + // Check RSI is in valid range [0, 100] + for (i, &value) in result.iter().enumerate() { + if !value.is_nan() { + assert!( + value >= 0.0 && value <= 100.0, + "RSI at index {} is out of range: {}", + i, + value + ); + } + } +} + +#[test] +fn test_macd_structure() { + let (_, _, _, close, _) = sample_ohlcv(); + let result = macd(&close, 12, 26, 9).unwrap(); + + assert_eq!(result.macd_line.len(), close.len()); + assert_eq!(result.signal_line.len(), close.len()); + assert_eq!(result.histogram.len(), close.len()); + + // MACD line should be valid from index 25 (slow_period - 1) + assert!(result.macd_line[24].is_nan()); + assert!(!result.macd_line[25].is_nan()); +} + +#[test] +fn test_stochastic_range() { + let (_, high, low, close, _) = sample_ohlcv(); + let result = stochastic(&high, &low, &close, 14, 3).unwrap(); + + // %K and %D should be in [0, 100] + for (i, &k) in result.k.iter().enumerate() { + if !k.is_nan() { + assert!( + k >= 0.0 && k <= 100.0, + "%K at index {} is out of range: {}", + i, + k + ); + } + } + + for (i, &d) in result.d.iter().enumerate() { + if !d.is_nan() { + assert!( + d >= 0.0 && d <= 100.0, + "%D at index {} is out of range: {}", + i, + d + ); + } + } +} + +#[test] +fn test_atr_positive() { + let (_, high, low, close, _) = sample_ohlcv(); + let result = atr(&high, &low, &close, 14).unwrap(); + + // ATR should always be non-negative + for (i, &value) in result.iter().enumerate() { + if !value.is_nan() { + assert!(value >= 0.0, "ATR at index {} is negative: {}", i, value); + } + } +} + +#[test] +fn test_bollinger_bands_ordering() { + let (_, _, _, close, _) = sample_ohlcv(); + let result = bollinger_bands(&close, 20, 2.0).unwrap(); + + // Upper > Middle > Lower + for i in 19..close.len() { + if !result.upper[i].is_nan() { + assert!( + result.upper[i] >= result.middle[i], + "Upper band should be >= middle at index {}", + i + ); + assert!( + result.middle[i] >= result.lower[i], + "Middle band should be >= lower at index {}", + i + ); + } + } +} + +#[test] +fn test_adx_range() { + let (_, high, low, close, _) = sample_ohlcv(); + let result = adx(&high, &low, &close, 14).unwrap(); + + // ADX should be in [0, 100] + for (i, &value) in result.iter().enumerate() { + if !value.is_nan() { + assert!( + value >= 0.0 && value <= 100.0, + "ADX at index {} is out of range: {}", + i, + value + ); + } + } +} + +#[test] +fn test_vwap_bounds() { + let (_, high, low, close, volume) = sample_ohlcv(); + let result = vwap(&high, &low, &close, &volume).unwrap(); + + // VWAP should be between the overall min low and max high + let min_low = low.iter().cloned().fold(f64::INFINITY, f64::min); + let max_high = high.iter().cloned().fold(f64::NEG_INFINITY, f64::max); + + for (i, &value) in result.iter().enumerate() { + if !value.is_nan() { + assert!( + value >= min_low && value <= max_high, + "VWAP at index {} is out of bounds: {} (should be between {} and {})", + i, + value, + min_low, + max_high + ); + } + } +} + +#[test] +fn test_supertrend_direction() { + let (_, high, low, close, _) = sample_ohlcv(); + let result = supertrend(&high, &low, &close, 10, 3.0).unwrap(); + + // Direction should be either 1 or -1 + for (i, &dir) in result.direction.iter().enumerate() { + if dir != 0 { + assert!( + dir == 1 || dir == -1, + "Supertrend direction at index {} is invalid: {}", + i, + dir + ); + } + } +} + +#[test] +fn test_invalid_period() { + let data = vec![1.0, 2.0, 3.0]; + + // Period of 0 should error + assert!(sma(&data, 0).is_err()); + assert!(ema(&data, 0).is_err()); + assert!(rsi(&data, 0).is_err()); +} + +#[test] +fn test_empty_data() { + let empty: Vec = vec![]; + + let result = sma(&empty, 10).unwrap(); + assert!(result.is_empty()); + + let result = ema(&empty, 10).unwrap(); + assert!(result.is_empty()); +} diff --git a/tests/test_portfolio.rs b/tests/test_portfolio.rs new file mode 100644 index 0000000..a919c49 --- /dev/null +++ b/tests/test_portfolio.rs @@ -0,0 +1,314 @@ +//! Integration tests for RaptorBT portfolio engine. + +use raptorbt::core::types::{ + BacktestConfig, CompiledSignals, Direction, OhlcvData, StopConfig, TargetConfig, +}; +use raptorbt::portfolio::engine::PortfolioEngine; + +fn sample_ohlcv() -> OhlcvData { + // Create trending sample data + let n = 100; + let mut close = vec![100.0]; + let mut open = vec![100.0]; + let mut high = vec![101.0]; + let mut low = vec![99.0]; + + for i in 1..n { + let trend = (i as f64) * 0.5; // Upward trend + let noise = ((i as f64) * 0.3).sin() * 2.0; + let new_close = 100.0 + trend + noise; + close.push(new_close); + open.push(close[i - 1]); + high.push(new_close + 1.0); + low.push(new_close - 1.0); + } + + OhlcvData { + timestamps: (0..n as i64).collect(), + open, + high, + low, + close, + volume: vec![1000.0; n], + } +} + +fn simple_signals(n: usize) -> CompiledSignals { + // Entry at bar 10, exit at bar 50 + let mut entries = vec![false; n]; + let mut exits = vec![false; n]; + entries[10] = true; + exits[50] = true; + + CompiledSignals { + symbol: "TEST".to_string(), + entries, + exits, + position_sizes: None, + direction: Direction::Long, + weight: 1.0, + } +} + +#[test] +fn test_basic_backtest() { + let ohlcv = sample_ohlcv(); + let signals = simple_signals(ohlcv.len()); + + let config = BacktestConfig::default(); + let engine = PortfolioEngine::new(config); + let result = engine.run_single(&ohlcv, &signals); + + // Should have 1 complete trade + assert_eq!(result.trades.len(), 1); + + // Equity curve should have same length as data + assert_eq!(result.equity_curve.len(), ohlcv.len()); + + // In an uptrend, should have positive return + assert!(result.metrics.total_return_pct > 0.0); +} + +#[test] +fn test_multiple_trades() { + let ohlcv = sample_ohlcv(); + let n = ohlcv.len(); + + // Multiple trades + let mut entries = vec![false; n]; + let mut exits = vec![false; n]; + entries[10] = true; + exits[20] = true; + entries[30] = true; + exits[40] = true; + entries[50] = true; + exits[60] = true; + + let signals = CompiledSignals { + symbol: "TEST".to_string(), + entries, + exits, + position_sizes: None, + direction: Direction::Long, + weight: 1.0, + }; + + let config = BacktestConfig::default(); + let engine = PortfolioEngine::new(config); + let result = engine.run_single(&ohlcv, &signals); + + // Should have 3 trades + assert_eq!(result.trades.len(), 3); +} + +#[test] +fn test_with_fees() { + let ohlcv = sample_ohlcv(); + let signals = simple_signals(ohlcv.len()); + + let config = BacktestConfig { + fees: 0.01, // 1% fee + ..Default::default() + }; + let engine = PortfolioEngine::new(config); + let result = engine.run_single(&ohlcv, &signals); + + // Trade should have fees deducted + assert!(result.trades[0].fees > 0.0); + + // Return should be lower due to fees + let config_no_fees = BacktestConfig::default(); + let engine_no_fees = PortfolioEngine::new(config_no_fees); + let result_no_fees = engine_no_fees.run_single(&ohlcv, &signals); + + assert!(result.metrics.end_value < result_no_fees.metrics.end_value); +} + +#[test] +fn test_fixed_stop_loss() { + let ohlcv = sample_ohlcv(); + let n = ohlcv.len(); + + // Entry at bar 10 + let mut entries = vec![false; n]; + entries[10] = true; + let exits = vec![false; n]; // No exit signal + + let signals = CompiledSignals { + symbol: "TEST".to_string(), + entries, + exits, + position_sizes: None, + direction: Direction::Long, + weight: 1.0, + }; + + let config = BacktestConfig { + stop: StopConfig::Fixed { percent: 0.02 }, // 2% stop + ..Default::default() + }; + let engine = PortfolioEngine::new(config); + let result = engine.run_single(&ohlcv, &signals); + + // Should have at least one trade (may exit on stop or end of data) + assert!(!result.trades.is_empty()); +} + +#[test] +fn test_fixed_take_profit() { + let ohlcv = sample_ohlcv(); + let n = ohlcv.len(); + + // Entry at bar 10 + let mut entries = vec![false; n]; + entries[10] = true; + let exits = vec![false; n]; // No exit signal + + let signals = CompiledSignals { + symbol: "TEST".to_string(), + entries, + exits, + position_sizes: None, + direction: Direction::Long, + weight: 1.0, + }; + + let config = BacktestConfig { + target: TargetConfig::Fixed { percent: 0.10 }, // 10% target + ..Default::default() + }; + let engine = PortfolioEngine::new(config); + let result = engine.run_single(&ohlcv, &signals); + + // Should have at least one trade + assert!(!result.trades.is_empty()); +} + +#[test] +fn test_no_trades() { + let ohlcv = sample_ohlcv(); + let n = ohlcv.len(); + + // No entry signals + let signals = CompiledSignals { + symbol: "TEST".to_string(), + entries: vec![false; n], + exits: vec![false; n], + position_sizes: None, + direction: Direction::Long, + weight: 1.0, + }; + + let config = BacktestConfig::default(); + let engine = PortfolioEngine::new(config); + let result = engine.run_single(&ohlcv, &signals); + + // Should have no trades + assert_eq!(result.trades.len(), 0); + assert_eq!(result.metrics.total_trades, 0); + + // Equity should remain at initial capital + assert!((result.metrics.end_value - result.metrics.start_value).abs() < 1e-10); +} + +#[test] +fn test_drawdown_positive() { + let ohlcv = sample_ohlcv(); + let signals = simple_signals(ohlcv.len()); + + let config = BacktestConfig::default(); + let engine = PortfolioEngine::new(config); + let result = engine.run_single(&ohlcv, &signals); + + // All drawdown values should be non-negative + for dd in &result.drawdown_curve { + assert!(*dd >= 0.0, "Drawdown should be non-negative"); + } +} + +#[test] +fn test_short_direction() { + // Create downtrend data + let n = 100; + let mut close = vec![100.0]; + for i in 1..n { + close.push(100.0 - (i as f64) * 0.3); // Downward trend + } + + let ohlcv = OhlcvData { + timestamps: (0..n as i64).collect(), + open: close + .iter() + .skip(1) + .chain(std::iter::once(&close[n - 1])) + .cloned() + .collect(), + high: close.iter().map(|c| c + 1.0).collect(), + low: close.iter().map(|c| c - 1.0).collect(), + close: close.clone(), + volume: vec![1000.0; n], + }; + + // Entry at bar 10, exit at bar 50 + let mut entries = vec![false; n]; + let mut exits = vec![false; n]; + entries[10] = true; + exits[50] = true; + + let signals = CompiledSignals { + symbol: "TEST".to_string(), + entries, + exits, + position_sizes: None, + direction: Direction::Short, // Short direction + weight: 1.0, + }; + + let config = BacktestConfig::default(); + let engine = PortfolioEngine::new(config); + let result = engine.run_single(&ohlcv, &signals); + + // Short in a downtrend should be profitable + assert!(result.trades[0].pnl > 0.0); +} + +#[test] +fn test_metrics_consistency() { + let ohlcv = sample_ohlcv(); + let n = ohlcv.len(); + + // Multiple trades for statistics + let mut entries = vec![false; n]; + let mut exits = vec![false; n]; + for i in (10..90).step_by(20) { + entries[i] = true; + exits[i + 10] = true; + } + + let signals = CompiledSignals { + symbol: "TEST".to_string(), + entries, + exits, + position_sizes: None, + direction: Direction::Long, + weight: 1.0, + }; + + let config = BacktestConfig::default(); + let engine = PortfolioEngine::new(config); + let result = engine.run_single(&ohlcv, &signals); + + // Total trades should equal winning + losing + assert_eq!( + result.metrics.total_trades, + result.metrics.winning_trades + result.metrics.losing_trades + ); + + // Win rate should be in [0, 100] + assert!(result.metrics.win_rate_pct >= 0.0); + assert!(result.metrics.win_rate_pct <= 100.0); + + // Exposure should be in [0, 100] + assert!(result.metrics.exposure_pct >= 0.0); + assert!(result.metrics.exposure_pct <= 100.0); +} diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..8a412b3 --- /dev/null +++ b/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 1 +requires-python = ">=3.10" + +[[package]] +name = "raptorbt" +version = "0.1.0" +source = { editable = "." }