fsb-core 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,224 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.5.1"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
10
+
11
+ [[package]]
12
+ name = "crossbeam-deque"
13
+ version = "0.8.8"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
16
+ dependencies = [
17
+ "crossbeam-epoch",
18
+ "crossbeam-utils",
19
+ ]
20
+
21
+ [[package]]
22
+ name = "crossbeam-epoch"
23
+ version = "0.9.21"
24
+ source = "registry+https://github.com/rust-lang/crates.io-index"
25
+ checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
26
+ dependencies = [
27
+ "crossbeam-utils",
28
+ ]
29
+
30
+ [[package]]
31
+ name = "crossbeam-utils"
32
+ version = "0.8.23"
33
+ source = "registry+https://github.com/rust-lang/crates.io-index"
34
+ checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
35
+
36
+ [[package]]
37
+ name = "either"
38
+ version = "1.19.0"
39
+ source = "registry+https://github.com/rust-lang/crates.io-index"
40
+ checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
41
+
42
+ [[package]]
43
+ name = "fsb_core"
44
+ version = "0.4.0"
45
+ dependencies = [
46
+ "pyo3",
47
+ "rayon",
48
+ ]
49
+
50
+ [[package]]
51
+ name = "heck"
52
+ version = "0.5.0"
53
+ source = "registry+https://github.com/rust-lang/crates.io-index"
54
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
55
+
56
+ [[package]]
57
+ name = "indoc"
58
+ version = "2.0.8"
59
+ source = "registry+https://github.com/rust-lang/crates.io-index"
60
+ checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1"
61
+ dependencies = [
62
+ "rustversion",
63
+ ]
64
+
65
+ [[package]]
66
+ name = "libc"
67
+ version = "0.2.191"
68
+ source = "registry+https://github.com/rust-lang/crates.io-index"
69
+ checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
70
+
71
+ [[package]]
72
+ name = "memoffset"
73
+ version = "0.9.1"
74
+ source = "registry+https://github.com/rust-lang/crates.io-index"
75
+ checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
76
+ dependencies = [
77
+ "autocfg",
78
+ ]
79
+
80
+ [[package]]
81
+ name = "once_cell"
82
+ version = "1.21.4"
83
+ source = "registry+https://github.com/rust-lang/crates.io-index"
84
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
85
+
86
+ [[package]]
87
+ name = "portable-atomic"
88
+ version = "1.15.0"
89
+ source = "registry+https://github.com/rust-lang/crates.io-index"
90
+ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
91
+
92
+ [[package]]
93
+ name = "proc-macro2"
94
+ version = "1.0.107"
95
+ source = "registry+https://github.com/rust-lang/crates.io-index"
96
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
97
+ dependencies = [
98
+ "unicode-ident",
99
+ ]
100
+
101
+ [[package]]
102
+ name = "pyo3"
103
+ version = "0.27.2"
104
+ source = "registry+https://github.com/rust-lang/crates.io-index"
105
+ checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d"
106
+ dependencies = [
107
+ "indoc",
108
+ "libc",
109
+ "memoffset",
110
+ "once_cell",
111
+ "portable-atomic",
112
+ "pyo3-build-config",
113
+ "pyo3-ffi",
114
+ "pyo3-macros",
115
+ "unindent",
116
+ ]
117
+
118
+ [[package]]
119
+ name = "pyo3-build-config"
120
+ version = "0.27.2"
121
+ source = "registry+https://github.com/rust-lang/crates.io-index"
122
+ checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6"
123
+ dependencies = [
124
+ "target-lexicon",
125
+ ]
126
+
127
+ [[package]]
128
+ name = "pyo3-ffi"
129
+ version = "0.27.2"
130
+ source = "registry+https://github.com/rust-lang/crates.io-index"
131
+ checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089"
132
+ dependencies = [
133
+ "libc",
134
+ "pyo3-build-config",
135
+ ]
136
+
137
+ [[package]]
138
+ name = "pyo3-macros"
139
+ version = "0.27.2"
140
+ source = "registry+https://github.com/rust-lang/crates.io-index"
141
+ checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02"
142
+ dependencies = [
143
+ "proc-macro2",
144
+ "pyo3-macros-backend",
145
+ "quote",
146
+ "syn",
147
+ ]
148
+
149
+ [[package]]
150
+ name = "pyo3-macros-backend"
151
+ version = "0.27.2"
152
+ source = "registry+https://github.com/rust-lang/crates.io-index"
153
+ checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9"
154
+ dependencies = [
155
+ "heck",
156
+ "proc-macro2",
157
+ "pyo3-build-config",
158
+ "quote",
159
+ "syn",
160
+ ]
161
+
162
+ [[package]]
163
+ name = "quote"
164
+ version = "1.0.47"
165
+ source = "registry+https://github.com/rust-lang/crates.io-index"
166
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
167
+ dependencies = [
168
+ "proc-macro2",
169
+ ]
170
+
171
+ [[package]]
172
+ name = "rayon"
173
+ version = "1.12.0"
174
+ source = "registry+https://github.com/rust-lang/crates.io-index"
175
+ checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
176
+ dependencies = [
177
+ "either",
178
+ "rayon-core",
179
+ ]
180
+
181
+ [[package]]
182
+ name = "rayon-core"
183
+ version = "1.13.0"
184
+ source = "registry+https://github.com/rust-lang/crates.io-index"
185
+ checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
186
+ dependencies = [
187
+ "crossbeam-deque",
188
+ "crossbeam-utils",
189
+ ]
190
+
191
+ [[package]]
192
+ name = "rustversion"
193
+ version = "1.0.23"
194
+ source = "registry+https://github.com/rust-lang/crates.io-index"
195
+ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
196
+
197
+ [[package]]
198
+ name = "syn"
199
+ version = "2.0.119"
200
+ source = "registry+https://github.com/rust-lang/crates.io-index"
201
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
202
+ dependencies = [
203
+ "proc-macro2",
204
+ "quote",
205
+ "unicode-ident",
206
+ ]
207
+
208
+ [[package]]
209
+ name = "target-lexicon"
210
+ version = "0.13.5"
211
+ source = "registry+https://github.com/rust-lang/crates.io-index"
212
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
213
+
214
+ [[package]]
215
+ name = "unicode-ident"
216
+ version = "1.0.27"
217
+ source = "registry+https://github.com/rust-lang/crates.io-index"
218
+ checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
219
+
220
+ [[package]]
221
+ name = "unindent"
222
+ version = "0.2.4"
223
+ source = "registry+https://github.com/rust-lang/crates.io-index"
224
+ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
@@ -0,0 +1,21 @@
1
+ [package]
2
+ name = "fsb_core"
3
+ version = "0.4.0"
4
+ edition = "2021"
5
+ description = "Native indicator and backtest helpers for ForexSmartBot (PyO3)"
6
+ license = "MIT"
7
+ authors = ["VoxHash <contact@voxhash.dev>"]
8
+ readme = "README.md"
9
+
10
+ [lib]
11
+ name = "fsb_core"
12
+ crate-type = ["cdylib"]
13
+
14
+ [dependencies]
15
+ pyo3 = { version = "0.27", features = ["extension-module"] }
16
+ rayon = "1.10"
17
+
18
+ [profile.release]
19
+ lto = true
20
+ codegen-units = 1
21
+ opt-level = 3
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: fsb-core
3
+ Version: 0.4.0
4
+ Classifier: Development Status :: 4 - Beta
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: Intended Audience :: Financial and Insurance Industry
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Programming Language :: Rust
9
+ Classifier: Programming Language :: Python :: Implementation :: CPython
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Office/Business :: Financial :: Investment
16
+ Summary: Native Rust indicator and equity helpers for ForexSmartBot (PyO3)
17
+ Keywords: forex,indicators,backtest,pyo3,rust
18
+ Author-email: VoxHash <contact@voxhash.dev>
19
+ License: MIT
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
22
+ Project-URL: Homepage, https://github.com/VoxHash/ForexSmartBot
23
+ Project-URL: Issues, https://github.com/VoxHash/ForexSmartBot/issues
24
+ Project-URL: Repository, https://github.com/VoxHash/ForexSmartBot
25
+
26
+ # fsb_core
27
+
28
+ Optional Rust (PyO3) extension for ForexSmartBot indicator and equity hot paths.
29
+
30
+ **0.4.0** adds EMA + MACD indicators and EMA/MACD crossover equity sweeps (on top of 0.3.x RSI mean-reversion and 0.2.x SMA walk-forward / multi-symbol workers).
31
+
32
+ ## Build
33
+
34
+ ```bash
35
+ # Requires rustup + maturin
36
+ pip install maturin
37
+ maturin develop --release
38
+ ```
39
+
40
+ ## Smoke test
41
+
42
+ ```bash
43
+ python -c "import fsb_core; print(fsb_core.__version__, fsb_core.ema([1,2,3,4,5], 3))"
44
+ python -c "from forexsmartbot.utils.native_indicators import sweep_ema_crossover, sweep_macd; import numpy as np; c=np.linspace(1.1,1.2,200); print(sweep_ema_crossover(c,[5,8],[15,20]).shape, sweep_macd(c,[8],[17],[5]).shape)"
45
+ ```
46
+
47
+ Python wrappers with NumPy fallback live in `forexsmartbot.utils.native_indicators`.
48
+
@@ -0,0 +1,22 @@
1
+ # fsb_core
2
+
3
+ Optional Rust (PyO3) extension for ForexSmartBot indicator and equity hot paths.
4
+
5
+ **0.4.0** adds EMA + MACD indicators and EMA/MACD crossover equity sweeps (on top of 0.3.x RSI mean-reversion and 0.2.x SMA walk-forward / multi-symbol workers).
6
+
7
+ ## Build
8
+
9
+ ```bash
10
+ # Requires rustup + maturin
11
+ pip install maturin
12
+ maturin develop --release
13
+ ```
14
+
15
+ ## Smoke test
16
+
17
+ ```bash
18
+ python -c "import fsb_core; print(fsb_core.__version__, fsb_core.ema([1,2,3,4,5], 3))"
19
+ python -c "from forexsmartbot.utils.native_indicators import sweep_ema_crossover, sweep_macd; import numpy as np; c=np.linspace(1.1,1.2,200); print(sweep_ema_crossover(c,[5,8],[15,20]).shape, sweep_macd(c,[8],[17],[5]).shape)"
20
+ ```
21
+
22
+ Python wrappers with NumPy fallback live in `forexsmartbot.utils.native_indicators`.
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["maturin>=1.7,<2.0"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "fsb-core"
7
+ version = "0.4.0"
8
+ description = "Native Rust indicator and equity helpers for ForexSmartBot (PyO3)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "VoxHash", email = "contact@voxhash.dev" }]
13
+ keywords = ["forex", "indicators", "backtest", "pyo3", "rust"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Financial and Insurance Industry",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Rust",
20
+ "Programming Language :: Python :: Implementation :: CPython",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Office/Business :: Financial :: Investment",
27
+ ]
28
+ urls = { Homepage = "https://github.com/VoxHash/ForexSmartBot", Repository = "https://github.com/VoxHash/ForexSmartBot", Issues = "https://github.com/VoxHash/ForexSmartBot/issues" }
29
+
30
+ [tool.maturin]
31
+ features = ["pyo3/extension-module"]
32
+ module-name = "fsb_core"
@@ -0,0 +1,882 @@
1
+ //! Native CPU-bound helpers for ForexSmartBot.
2
+ //!
3
+ //! Implements rolling indicators (SMA, EMA, ATR, RSI, MACD), batch SMA via rayon,
4
+ //! SMA/EMA-crossover equity, MACD-signal crossover equity, walk-forward sweeps,
5
+ //! multi-symbol portfolio aggregation, and RSI mean-reversion equity/sweeps so
6
+ //! indicator/backtest hot paths can run with lower overhead than pure
7
+ //! pandas while the PyQt UI stays responsive.
8
+
9
+ use pyo3::prelude::*;
10
+ use pyo3::exceptions::PyValueError;
11
+ use rayon::prelude::*;
12
+
13
+ /// O(n) simple moving average. Leading `window-1` samples are NaN.
14
+ fn sma_slice(values: &[f64], window: usize) -> Vec<f64> {
15
+ let n = values.len();
16
+ let mut out = vec![f64::NAN; n];
17
+ if window == 0 || n < window {
18
+ return out;
19
+ }
20
+ let mut sum = 0.0_f64;
21
+ for i in 0..n {
22
+ sum += values[i];
23
+ if i + 1 >= window {
24
+ if i + 1 > window {
25
+ sum -= values[i - window];
26
+ }
27
+ out[i] = sum / window as f64;
28
+ }
29
+ }
30
+ out
31
+ }
32
+
33
+
34
+ /// Exponential moving average. Leading `period-1` samples are NaN; seed uses SMA.
35
+ fn ema_slice(values: &[f64], period: usize) -> Vec<f64> {
36
+ let n = values.len();
37
+ let mut out = vec![f64::NAN; n];
38
+ if period == 0 || n < period {
39
+ return out;
40
+ }
41
+ let mut sum = 0.0_f64;
42
+ for i in 0..period {
43
+ sum += values[i];
44
+ }
45
+ let mut prev = sum / period as f64;
46
+ out[period - 1] = prev;
47
+ let alpha = 2.0 / (period as f64 + 1.0);
48
+ for i in period..n {
49
+ prev = alpha * values[i] + (1.0 - alpha) * prev;
50
+ out[i] = prev;
51
+ }
52
+ out
53
+ }
54
+
55
+ /// MACD line, signal line, and histogram. Fast/slow/signal periods must be >= 1
56
+ /// and `fast < slow`.
57
+ fn macd_slices(
58
+ close: &[f64],
59
+ fast: usize,
60
+ slow: usize,
61
+ signal: usize,
62
+ ) -> Result<(Vec<f64>, Vec<f64>, Vec<f64>), String> {
63
+ if fast == 0 || slow == 0 || signal == 0 || fast >= slow {
64
+ return Err("require fast >= 1, slow >= 1, signal >= 1, and fast < slow".into());
65
+ }
66
+ let ema_f = ema_slice(close, fast);
67
+ let ema_s = ema_slice(close, slow);
68
+ let n = close.len();
69
+ let mut macd = vec![f64::NAN; n];
70
+ for i in 0..n {
71
+ let f = ema_f[i];
72
+ let s = ema_s[i];
73
+ if !f.is_nan() && !s.is_nan() {
74
+ macd[i] = f - s;
75
+ }
76
+ }
77
+ // Signal EMA over the MACD series, skipping leading NaNs.
78
+ let mut signal_line = vec![f64::NAN; n];
79
+ let mut hist = vec![f64::NAN; n];
80
+ let mut seed_sum = 0.0_f64;
81
+ let mut seed_count = 0_usize;
82
+ let mut prev_signal = f64::NAN;
83
+ let alpha = 2.0 / (signal as f64 + 1.0);
84
+ for i in 0..n {
85
+ let m = macd[i];
86
+ if m.is_nan() {
87
+ continue;
88
+ }
89
+ if seed_count < signal {
90
+ seed_sum += m;
91
+ seed_count += 1;
92
+ if seed_count == signal {
93
+ prev_signal = seed_sum / signal as f64;
94
+ signal_line[i] = prev_signal;
95
+ hist[i] = m - prev_signal;
96
+ }
97
+ continue;
98
+ }
99
+ prev_signal = alpha * m + (1.0 - alpha) * prev_signal;
100
+ signal_line[i] = prev_signal;
101
+ hist[i] = m - prev_signal;
102
+ }
103
+ Ok((macd, signal_line, hist))
104
+ }
105
+ /// Average True Range over OHLC series (Wilder-style simple mean of TR).
106
+ fn atr_slice(high: &[f64], low: &[f64], close: &[f64], period: usize) -> Result<Vec<f64>, String> {
107
+ let n = high.len();
108
+ if low.len() != n || close.len() != n {
109
+ return Err("high, low, and close must have the same length".into());
110
+ }
111
+ let mut tr = vec![0.0_f64; n];
112
+ for i in 0..n {
113
+ let hl = high[i] - low[i];
114
+ if i == 0 {
115
+ tr[i] = hl;
116
+ } else {
117
+ let hc = (high[i] - close[i - 1]).abs();
118
+ let lc = (low[i] - close[i - 1]).abs();
119
+ tr[i] = hl.max(hc).max(lc);
120
+ }
121
+ }
122
+ Ok(sma_slice(&tr, period))
123
+ }
124
+
125
+ /// Classic RSI using simple rolling mean of gains/losses.
126
+ fn rsi_slice(values: &[f64], period: usize) -> Vec<f64> {
127
+ let n = values.len();
128
+ let mut out = vec![f64::NAN; n];
129
+ if period == 0 || n < period + 1 {
130
+ return out;
131
+ }
132
+ let mut gains = vec![0.0_f64; n];
133
+ let mut losses = vec![0.0_f64; n];
134
+ for i in 1..n {
135
+ let d = values[i] - values[i - 1];
136
+ if d >= 0.0 {
137
+ gains[i] = d;
138
+ } else {
139
+ losses[i] = -d;
140
+ }
141
+ }
142
+ let avg_gain = sma_slice(&gains, period);
143
+ let avg_loss = sma_slice(&losses, period);
144
+ for i in 0..n {
145
+ let g = avg_gain[i];
146
+ let l = avg_loss[i];
147
+ if g.is_nan() || l.is_nan() {
148
+ continue;
149
+ }
150
+ if l == 0.0 {
151
+ out[i] = if g == 0.0 { 50.0 } else { 100.0 };
152
+ } else {
153
+ let rs = g / l;
154
+ out[i] = 100.0 - (100.0 / (1.0 + rs));
155
+ }
156
+ }
157
+ out
158
+ }
159
+
160
+ /// Compact SMA crossover equity over aligned series. Returns mark-to-market curve.
161
+ fn equity_sma_crossover_slice(
162
+ close: &[f64],
163
+ sma_fast: &[f64],
164
+ sma_slow: &[f64],
165
+ initial_balance: f64,
166
+ position_fraction: f64,
167
+ ) -> Vec<f64> {
168
+ let n = close.len();
169
+ let mut balance = initial_balance;
170
+ let mut side: i8 = 0;
171
+ let mut size = 0.0_f64;
172
+ let mut entry = 0.0_f64;
173
+ let mut equity = Vec::with_capacity(n);
174
+
175
+ for i in 0..n {
176
+ let px = close[i];
177
+ if i >= 1 {
178
+ let f = sma_fast[i];
179
+ let s = sma_slow[i];
180
+ let pf = sma_fast[i - 1];
181
+ let ps = sma_slow[i - 1];
182
+ if !f.is_nan() && !s.is_nan() && !pf.is_nan() && !ps.is_nan() {
183
+ let cross_up = f > s && pf <= ps;
184
+ let cross_dn = f < s && pf >= ps;
185
+ if cross_up && side <= 0 {
186
+ if side < 0 {
187
+ balance += -size * (px - entry);
188
+ }
189
+ size = (balance * position_fraction) / px;
190
+ entry = px;
191
+ side = 1;
192
+ } else if cross_dn && side >= 0 {
193
+ if side > 0 {
194
+ balance += size * (px - entry);
195
+ }
196
+ size = (balance * position_fraction) / px;
197
+ entry = px;
198
+ side = -1;
199
+ }
200
+ }
201
+ }
202
+ let mtm = if side != 0 {
203
+ side as f64 * size * (px - entry)
204
+ } else {
205
+ 0.0
206
+ };
207
+ equity.push(balance + mtm);
208
+ }
209
+ equity
210
+ }
211
+
212
+ fn max_drawdown(equity: &[f64]) -> f64 {
213
+ let mut peak = f64::NEG_INFINITY;
214
+ let mut max_dd = 0.0_f64;
215
+ for &e in equity {
216
+ if e > peak {
217
+ peak = e;
218
+ }
219
+ if peak > 0.0 {
220
+ let dd = (peak - e) / peak;
221
+ if dd > max_dd {
222
+ max_dd = dd;
223
+ }
224
+ }
225
+ }
226
+ max_dd
227
+ }
228
+
229
+ /// Run one SMA-crossover backtest; return (final_equity, total_return, max_drawdown).
230
+ fn sma_crossover_metrics(
231
+ close: &[f64],
232
+ fast: usize,
233
+ slow: usize,
234
+ initial_balance: f64,
235
+ position_fraction: f64,
236
+ ) -> (f64, f64, f64) {
237
+ if close.is_empty() || fast == 0 || slow == 0 || fast >= slow {
238
+ return (initial_balance, 0.0, 0.0);
239
+ }
240
+ let sma_f = sma_slice(close, fast);
241
+ let sma_s = sma_slice(close, slow);
242
+ let equity = equity_sma_crossover_slice(close, &sma_f, &sma_s, initial_balance, position_fraction);
243
+ let final_eq = *equity.last().unwrap_or(&initial_balance);
244
+ let total_return = if initial_balance != 0.0 {
245
+ (final_eq - initial_balance) / initial_balance
246
+ } else {
247
+ 0.0
248
+ };
249
+ (final_eq, total_return, max_drawdown(&equity))
250
+ }
251
+
252
+ fn build_param_pairs(fast_periods: &[usize], slow_periods: &[usize]) -> Vec<(usize, usize)> {
253
+ let mut pairs = Vec::new();
254
+ for &f in fast_periods {
255
+ for &s in slow_periods {
256
+ if f >= 1 && s >= 1 && f < s {
257
+ pairs.push((f, s));
258
+ }
259
+ }
260
+ }
261
+ pairs
262
+ }
263
+
264
+ #[pyfunction]
265
+ fn sma(values: Vec<f64>, window: usize) -> PyResult<Vec<f64>> {
266
+ if window == 0 {
267
+ return Err(PyValueError::new_err("window must be >= 1"));
268
+ }
269
+ Ok(sma_slice(&values, window))
270
+ }
271
+
272
+ #[pyfunction]
273
+ fn atr(high: Vec<f64>, low: Vec<f64>, close: Vec<f64>, period: usize) -> PyResult<Vec<f64>> {
274
+ if period == 0 {
275
+ return Err(PyValueError::new_err("period must be >= 1"));
276
+ }
277
+ atr_slice(&high, &low, &close, period).map_err(PyValueError::new_err)
278
+ }
279
+
280
+ #[pyfunction]
281
+ fn rsi(values: Vec<f64>, period: usize) -> PyResult<Vec<f64>> {
282
+ if period == 0 {
283
+ return Err(PyValueError::new_err("period must be >= 1"));
284
+ }
285
+ Ok(rsi_slice(&values, period))
286
+ }
287
+
288
+ /// Compute SMA for many series in parallel (rayon). Each series shares `window`.
289
+ #[pyfunction]
290
+ fn batch_sma(series: Vec<Vec<f64>>, window: usize) -> PyResult<Vec<Vec<f64>>> {
291
+ if window == 0 {
292
+ return Err(PyValueError::new_err("window must be >= 1"));
293
+ }
294
+ let out: Vec<Vec<f64>> = series
295
+ .par_iter()
296
+ .map(|s| sma_slice(s, window))
297
+ .collect();
298
+ Ok(out)
299
+ }
300
+
301
+ /// Compact SMA crossover backtest loop over precomputed series (no allocations per bar).
302
+ /// Returns mark-to-market equity curve starting from `initial_balance`.
303
+ #[pyfunction]
304
+ fn equity_sma_crossover(
305
+ close: Vec<f64>,
306
+ sma_fast: Vec<f64>,
307
+ sma_slow: Vec<f64>,
308
+ initial_balance: f64,
309
+ position_fraction: f64,
310
+ ) -> PyResult<Vec<f64>> {
311
+ let n = close.len();
312
+ if sma_fast.len() != n || sma_slow.len() != n {
313
+ return Err(PyValueError::new_err("close, sma_fast, and sma_slow lengths must match"));
314
+ }
315
+ if n == 0 {
316
+ return Ok(Vec::new());
317
+ }
318
+ Ok(equity_sma_crossover_slice(
319
+ &close,
320
+ &sma_fast,
321
+ &sma_slow,
322
+ initial_balance,
323
+ position_fraction,
324
+ ))
325
+ }
326
+
327
+ /// Parallel SMA-crossover parameter grid sweep.
328
+ ///
329
+ /// Each result row: `[fast, slow, final_equity, total_return, max_drawdown]`.
330
+ /// Only pairs with `fast < slow` are evaluated (rayon over the grid).
331
+ #[pyfunction]
332
+ fn sweep_sma_crossover(
333
+ close: Vec<f64>,
334
+ fast_periods: Vec<usize>,
335
+ slow_periods: Vec<usize>,
336
+ initial_balance: f64,
337
+ position_fraction: f64,
338
+ ) -> PyResult<Vec<Vec<f64>>> {
339
+ if close.is_empty() {
340
+ return Ok(Vec::new());
341
+ }
342
+ let pairs = build_param_pairs(&fast_periods, &slow_periods);
343
+ let out: Vec<Vec<f64>> = pairs
344
+ .par_iter()
345
+ .map(|&(fast, slow)| {
346
+ let (final_eq, ret, dd) =
347
+ sma_crossover_metrics(&close, fast, slow, initial_balance, position_fraction);
348
+ vec![fast as f64, slow as f64, final_eq, ret, dd]
349
+ })
350
+ .collect();
351
+ Ok(out)
352
+ }
353
+
354
+ /// Walk-forward SMA parameter sweep.
355
+ ///
356
+ /// `windows` rows are `[train_start, train_end, test_start, test_end]` (end exclusive).
357
+ /// For each window: grid-search on the train slice (max total_return), evaluate best
358
+ /// pair on the test slice. Result row:
359
+ /// `[best_fast, best_slow, train_return, test_return, test_final_equity, test_max_dd]`.
360
+ #[pyfunction]
361
+ fn walk_forward_sma_sweep(
362
+ close: Vec<f64>,
363
+ fast_periods: Vec<usize>,
364
+ slow_periods: Vec<usize>,
365
+ windows: Vec<Vec<usize>>,
366
+ initial_balance: f64,
367
+ position_fraction: f64,
368
+ ) -> PyResult<Vec<Vec<f64>>> {
369
+ let n = close.len();
370
+ let pairs = build_param_pairs(&fast_periods, &slow_periods);
371
+ if pairs.is_empty() {
372
+ return Ok(Vec::new());
373
+ }
374
+
375
+ let mut parsed: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(windows.len());
376
+ for (i, w) in windows.iter().enumerate() {
377
+ if w.len() != 4 {
378
+ return Err(PyValueError::new_err(format!(
379
+ "window {i} must have 4 indices [train_start, train_end, test_start, test_end]"
380
+ )));
381
+ }
382
+ let (ts, te, xs, xe) = (w[0], w[1], w[2], w[3]);
383
+ if te > n || xe > n || ts >= te || xs >= xe {
384
+ return Err(PyValueError::new_err(format!(
385
+ "window {i} has invalid bounds for series length {n}"
386
+ )));
387
+ }
388
+ parsed.push((ts, te, xs, xe));
389
+ }
390
+
391
+ let out: Vec<Vec<f64>> = parsed
392
+ .par_iter()
393
+ .map(|&(ts, te, xs, xe)| {
394
+ let train = &close[ts..te];
395
+ let mut best_fast = pairs[0].0;
396
+ let mut best_slow = pairs[0].1;
397
+ let mut best_train_ret = f64::NEG_INFINITY;
398
+ for &(fast, slow) in &pairs {
399
+ let (_eq, ret, _dd) =
400
+ sma_crossover_metrics(train, fast, slow, initial_balance, position_fraction);
401
+ if ret > best_train_ret {
402
+ best_train_ret = ret;
403
+ best_fast = fast;
404
+ best_slow = slow;
405
+ }
406
+ }
407
+ let test = &close[xs..xe];
408
+ let (test_eq, test_ret, test_dd) = sma_crossover_metrics(
409
+ test,
410
+ best_fast,
411
+ best_slow,
412
+ initial_balance,
413
+ position_fraction,
414
+ );
415
+ vec![
416
+ best_fast as f64,
417
+ best_slow as f64,
418
+ best_train_ret,
419
+ test_ret,
420
+ test_eq,
421
+ test_dd,
422
+ ]
423
+ })
424
+ .collect();
425
+ Ok(out)
426
+ }
427
+
428
+ /// Multi-symbol SMA-crossover workers + portfolio aggregation.
429
+ ///
430
+ /// Runs each symbol equity in parallel, then builds a weighted portfolio equity
431
+ /// (equal weight when `weights` is empty). Lengths must match across symbols.
432
+ ///
433
+ /// Returns `(per_symbol_equity, portfolio_equity)`.
434
+ #[pyfunction]
435
+ fn multi_symbol_equity_aggregate(
436
+ closes: Vec<Vec<f64>>,
437
+ fast_period: usize,
438
+ slow_period: usize,
439
+ initial_balance: f64,
440
+ position_fraction: f64,
441
+ weights: Vec<f64>,
442
+ ) -> PyResult<(Vec<Vec<f64>>, Vec<f64>)> {
443
+ if closes.is_empty() {
444
+ return Ok((Vec::new(), Vec::new()));
445
+ }
446
+ if fast_period == 0 || slow_period == 0 || fast_period >= slow_period {
447
+ return Err(PyValueError::new_err(
448
+ "require fast_period >= 1, slow_period >= 1, and fast_period < slow_period",
449
+ ));
450
+ }
451
+ let n = closes[0].len();
452
+ for (i, c) in closes.iter().enumerate() {
453
+ if c.len() != n {
454
+ return Err(PyValueError::new_err(format!(
455
+ "symbol {i} length {} != first symbol length {n}",
456
+ c.len()
457
+ )));
458
+ }
459
+ }
460
+
461
+ let m = closes.len();
462
+ let mut w = weights;
463
+ if w.is_empty() {
464
+ w = vec![1.0 / m as f64; m];
465
+ } else if w.len() != m {
466
+ return Err(PyValueError::new_err(
467
+ "weights length must match number of symbols (or be empty for equal weight)",
468
+ ));
469
+ }
470
+ let wsum: f64 = w.iter().sum();
471
+ if wsum <= 0.0 || !wsum.is_finite() {
472
+ return Err(PyValueError::new_err("weights must sum to a positive finite value"));
473
+ }
474
+ for wi in &mut w {
475
+ *wi /= wsum;
476
+ }
477
+
478
+ let per_symbol: Vec<Vec<f64>> = closes
479
+ .par_iter()
480
+ .map(|close| {
481
+ let sma_f = sma_slice(close, fast_period);
482
+ let sma_s = sma_slice(close, slow_period);
483
+ equity_sma_crossover_slice(close, &sma_f, &sma_s, initial_balance, position_fraction)
484
+ })
485
+ .collect();
486
+
487
+ let mut portfolio = vec![0.0_f64; n];
488
+ for (sym_eq, &wi) in per_symbol.iter().zip(w.iter()) {
489
+ for i in 0..n {
490
+ // Normalize each curve to returns vs initial, then re-scale to portfolio capital.
491
+ let ret = if initial_balance != 0.0 {
492
+ (sym_eq[i] / initial_balance) - 1.0
493
+ } else {
494
+ 0.0
495
+ };
496
+ portfolio[i] += wi * initial_balance * (1.0 + ret);
497
+ }
498
+ }
499
+ Ok((per_symbol, portfolio))
500
+ }
501
+
502
+ /// RSI mean-reversion equity: long when RSI crosses up through `oversold`,
503
+ /// short when RSI crosses down through `overbought`.
504
+ fn equity_rsi_mean_reversion_slice(
505
+ close: &[f64],
506
+ rsi: &[f64],
507
+ oversold: f64,
508
+ overbought: f64,
509
+ initial_balance: f64,
510
+ position_fraction: f64,
511
+ ) -> Vec<f64> {
512
+ let n = close.len();
513
+ let mut balance = initial_balance;
514
+ let mut side: i8 = 0;
515
+ let mut size = 0.0_f64;
516
+ let mut entry = 0.0_f64;
517
+ let mut equity = Vec::with_capacity(n);
518
+
519
+ for i in 0..n {
520
+ let px = close[i];
521
+ if i >= 1 {
522
+ let r = rsi[i];
523
+ let pr = rsi[i - 1];
524
+ if !r.is_nan() && !pr.is_nan() {
525
+ let cross_up = pr <= oversold && r > oversold;
526
+ let cross_dn = pr >= overbought && r < overbought;
527
+ if cross_up && side <= 0 {
528
+ if side < 0 {
529
+ balance += -size * (px - entry);
530
+ }
531
+ size = (balance * position_fraction) / px;
532
+ entry = px;
533
+ side = 1;
534
+ } else if cross_dn && side >= 0 {
535
+ if side > 0 {
536
+ balance += size * (px - entry);
537
+ }
538
+ size = (balance * position_fraction) / px;
539
+ entry = px;
540
+ side = -1;
541
+ }
542
+ }
543
+ }
544
+ let mtm = if side != 0 {
545
+ side as f64 * size * (px - entry)
546
+ } else {
547
+ 0.0
548
+ };
549
+ equity.push(balance + mtm);
550
+ }
551
+ equity
552
+ }
553
+
554
+ fn rsi_mean_reversion_metrics(
555
+ close: &[f64],
556
+ period: usize,
557
+ oversold: f64,
558
+ overbought: f64,
559
+ initial_balance: f64,
560
+ position_fraction: f64,
561
+ ) -> (f64, f64, f64) {
562
+ if close.is_empty()
563
+ || period == 0
564
+ || !(oversold.is_finite() && overbought.is_finite())
565
+ || oversold >= overbought
566
+ {
567
+ return (initial_balance, 0.0, 0.0);
568
+ }
569
+ let rsi_vals = rsi_slice(close, period);
570
+ let equity = equity_rsi_mean_reversion_slice(
571
+ close,
572
+ &rsi_vals,
573
+ oversold,
574
+ overbought,
575
+ initial_balance,
576
+ position_fraction,
577
+ );
578
+ let final_eq = *equity.last().unwrap_or(&initial_balance);
579
+ let total_return = if initial_balance != 0.0 {
580
+ (final_eq - initial_balance) / initial_balance
581
+ } else {
582
+ 0.0
583
+ };
584
+ (final_eq, total_return, max_drawdown(&equity))
585
+ }
586
+
587
+ /// Compact RSI mean-reversion equity over aligned close/RSI series.
588
+ #[pyfunction]
589
+ fn equity_rsi_mean_reversion(
590
+ close: Vec<f64>,
591
+ rsi: Vec<f64>,
592
+ oversold: f64,
593
+ overbought: f64,
594
+ initial_balance: f64,
595
+ position_fraction: f64,
596
+ ) -> PyResult<Vec<f64>> {
597
+ let n = close.len();
598
+ if rsi.len() != n {
599
+ return Err(PyValueError::new_err("close and rsi lengths must match"));
600
+ }
601
+ if !(oversold.is_finite() && overbought.is_finite()) || oversold >= overbought {
602
+ return Err(PyValueError::new_err(
603
+ "require finite oversold < overbought",
604
+ ));
605
+ }
606
+ if n == 0 {
607
+ return Ok(Vec::new());
608
+ }
609
+ Ok(equity_rsi_mean_reversion_slice(
610
+ &close,
611
+ &rsi,
612
+ oversold,
613
+ overbought,
614
+ initial_balance,
615
+ position_fraction,
616
+ ))
617
+ }
618
+
619
+ /// Parallel RSI mean-reversion parameter sweep.
620
+ ///
621
+ /// Each result row:
622
+ /// `[period, oversold, overbought, final_equity, total_return, max_drawdown]`.
623
+ /// Only triples with `period >= 1` and `oversold < overbought` are evaluated.
624
+ #[pyfunction]
625
+ fn sweep_rsi_mean_reversion(
626
+ close: Vec<f64>,
627
+ rsi_periods: Vec<usize>,
628
+ oversold_levels: Vec<f64>,
629
+ overbought_levels: Vec<f64>,
630
+ initial_balance: f64,
631
+ position_fraction: f64,
632
+ ) -> PyResult<Vec<Vec<f64>>> {
633
+ if close.is_empty() {
634
+ return Ok(Vec::new());
635
+ }
636
+ let mut triples: Vec<(usize, f64, f64)> = Vec::new();
637
+ for &period in &rsi_periods {
638
+ if period == 0 {
639
+ continue;
640
+ }
641
+ for &os in &oversold_levels {
642
+ for &ob in &overbought_levels {
643
+ if os.is_finite() && ob.is_finite() && os < ob {
644
+ triples.push((period, os, ob));
645
+ }
646
+ }
647
+ }
648
+ }
649
+ let out: Vec<Vec<f64>> = triples
650
+ .par_iter()
651
+ .map(|&(period, os, ob)| {
652
+ let (final_eq, ret, dd) = rsi_mean_reversion_metrics(
653
+ &close,
654
+ period,
655
+ os,
656
+ ob,
657
+ initial_balance,
658
+ position_fraction,
659
+ );
660
+ vec![period as f64, os, ob, final_eq, ret, dd]
661
+ })
662
+ .collect();
663
+ Ok(out)
664
+ }
665
+
666
+
667
+ /// EMA-crossover metrics (final_equity, total_return, max_drawdown).
668
+ fn ema_crossover_metrics(
669
+ close: &[f64],
670
+ fast: usize,
671
+ slow: usize,
672
+ initial_balance: f64,
673
+ position_fraction: f64,
674
+ ) -> (f64, f64, f64) {
675
+ if close.is_empty() || fast == 0 || slow == 0 || fast >= slow {
676
+ return (initial_balance, 0.0, 0.0);
677
+ }
678
+ let ema_f = ema_slice(close, fast);
679
+ let ema_s = ema_slice(close, slow);
680
+ let equity = equity_sma_crossover_slice(close, &ema_f, &ema_s, initial_balance, position_fraction);
681
+ let final_eq = *equity.last().unwrap_or(&initial_balance);
682
+ let total_return = if initial_balance != 0.0 {
683
+ (final_eq - initial_balance) / initial_balance
684
+ } else {
685
+ 0.0
686
+ };
687
+ (final_eq, total_return, max_drawdown(&equity))
688
+ }
689
+
690
+ /// MACD vs signal crossover equity (same side logic as SMA crossover).
691
+ fn equity_macd_crossover_slice(
692
+ close: &[f64],
693
+ macd: &[f64],
694
+ signal: &[f64],
695
+ initial_balance: f64,
696
+ position_fraction: f64,
697
+ ) -> Vec<f64> {
698
+ equity_sma_crossover_slice(close, macd, signal, initial_balance, position_fraction)
699
+ }
700
+
701
+ fn macd_crossover_metrics(
702
+ close: &[f64],
703
+ fast: usize,
704
+ slow: usize,
705
+ signal: usize,
706
+ initial_balance: f64,
707
+ position_fraction: f64,
708
+ ) -> (f64, f64, f64) {
709
+ if close.is_empty() {
710
+ return (initial_balance, 0.0, 0.0);
711
+ }
712
+ let Ok((macd, sig, _)) = macd_slices(close, fast, slow, signal) else {
713
+ return (initial_balance, 0.0, 0.0);
714
+ };
715
+ let equity = equity_macd_crossover_slice(
716
+ close,
717
+ &macd,
718
+ &sig,
719
+ initial_balance,
720
+ position_fraction,
721
+ );
722
+ let final_eq = *equity.last().unwrap_or(&initial_balance);
723
+ let total_return = if initial_balance != 0.0 {
724
+ (final_eq - initial_balance) / initial_balance
725
+ } else {
726
+ 0.0
727
+ };
728
+ (final_eq, total_return, max_drawdown(&equity))
729
+ }
730
+
731
+ #[pyfunction]
732
+ fn ema(values: Vec<f64>, period: usize) -> PyResult<Vec<f64>> {
733
+ if period == 0 {
734
+ return Err(PyValueError::new_err("period must be >= 1"));
735
+ }
736
+ Ok(ema_slice(&values, period))
737
+ }
738
+
739
+ /// Returns `(macd_line, signal_line, histogram)`.
740
+ #[pyfunction]
741
+ fn macd(
742
+ close: Vec<f64>,
743
+ fast_period: usize,
744
+ slow_period: usize,
745
+ signal_period: usize,
746
+ ) -> PyResult<(Vec<f64>, Vec<f64>, Vec<f64>)> {
747
+ macd_slices(&close, fast_period, slow_period, signal_period).map_err(PyValueError::new_err)
748
+ }
749
+
750
+ /// Compact EMA crossover equity over aligned close/EMA series.
751
+ #[pyfunction]
752
+ fn equity_ema_crossover(
753
+ close: Vec<f64>,
754
+ ema_fast: Vec<f64>,
755
+ ema_slow: Vec<f64>,
756
+ initial_balance: f64,
757
+ position_fraction: f64,
758
+ ) -> PyResult<Vec<f64>> {
759
+ equity_sma_crossover(close, ema_fast, ema_slow, initial_balance, position_fraction)
760
+ }
761
+
762
+ /// Parallel EMA-crossover parameter grid sweep.
763
+ ///
764
+ /// Each result row: `[fast, slow, final_equity, total_return, max_drawdown]`.
765
+ #[pyfunction]
766
+ fn sweep_ema_crossover(
767
+ close: Vec<f64>,
768
+ fast_periods: Vec<usize>,
769
+ slow_periods: Vec<usize>,
770
+ initial_balance: f64,
771
+ position_fraction: f64,
772
+ ) -> PyResult<Vec<Vec<f64>>> {
773
+ if close.is_empty() {
774
+ return Ok(Vec::new());
775
+ }
776
+ let pairs = build_param_pairs(&fast_periods, &slow_periods);
777
+ let out: Vec<Vec<f64>> = pairs
778
+ .par_iter()
779
+ .map(|&(fast, slow)| {
780
+ let (final_eq, ret, dd) =
781
+ ema_crossover_metrics(&close, fast, slow, initial_balance, position_fraction);
782
+ vec![fast as f64, slow as f64, final_eq, ret, dd]
783
+ })
784
+ .collect();
785
+ Ok(out)
786
+ }
787
+
788
+ /// Compact MACD vs signal crossover equity.
789
+ #[pyfunction]
790
+ fn equity_macd_crossover(
791
+ close: Vec<f64>,
792
+ macd_line: Vec<f64>,
793
+ signal_line: Vec<f64>,
794
+ initial_balance: f64,
795
+ position_fraction: f64,
796
+ ) -> PyResult<Vec<f64>> {
797
+ let n = close.len();
798
+ if macd_line.len() != n || signal_line.len() != n {
799
+ return Err(PyValueError::new_err(
800
+ "close, macd_line, and signal_line lengths must match",
801
+ ));
802
+ }
803
+ if n == 0 {
804
+ return Ok(Vec::new());
805
+ }
806
+ Ok(equity_macd_crossover_slice(
807
+ &close,
808
+ &macd_line,
809
+ &signal_line,
810
+ initial_balance,
811
+ position_fraction,
812
+ ))
813
+ }
814
+
815
+ /// Parallel MACD parameter sweep.
816
+ ///
817
+ /// Each result row:
818
+ /// `[fast, slow, signal, final_equity, total_return, max_drawdown]`.
819
+ /// Only triples with `fast < slow` and all periods >= 1 are evaluated.
820
+ #[pyfunction]
821
+ fn sweep_macd(
822
+ close: Vec<f64>,
823
+ fast_periods: Vec<usize>,
824
+ slow_periods: Vec<usize>,
825
+ signal_periods: Vec<usize>,
826
+ initial_balance: f64,
827
+ position_fraction: f64,
828
+ ) -> PyResult<Vec<Vec<f64>>> {
829
+ if close.is_empty() {
830
+ return Ok(Vec::new());
831
+ }
832
+ let mut triples: Vec<(usize, usize, usize)> = Vec::new();
833
+ for &fast in &fast_periods {
834
+ for &slow in &slow_periods {
835
+ if fast == 0 || slow == 0 || fast >= slow {
836
+ continue;
837
+ }
838
+ for &sig in &signal_periods {
839
+ if sig >= 1 {
840
+ triples.push((fast, slow, sig));
841
+ }
842
+ }
843
+ }
844
+ }
845
+ let out: Vec<Vec<f64>> = triples
846
+ .par_iter()
847
+ .map(|&(fast, slow, sig)| {
848
+ let (final_eq, ret, dd) = macd_crossover_metrics(
849
+ &close,
850
+ fast,
851
+ slow,
852
+ sig,
853
+ initial_balance,
854
+ position_fraction,
855
+ );
856
+ vec![fast as f64, slow as f64, sig as f64, final_eq, ret, dd]
857
+ })
858
+ .collect();
859
+ Ok(out)
860
+ }
861
+
862
+ #[pymodule]
863
+ fn fsb_core(m: &Bound<'_, PyModule>) -> PyResult<()> {
864
+ m.add("__version__", env!("CARGO_PKG_VERSION"))?;
865
+ m.add_function(wrap_pyfunction!(sma, m)?)?;
866
+ m.add_function(wrap_pyfunction!(atr, m)?)?;
867
+ m.add_function(wrap_pyfunction!(rsi, m)?)?;
868
+ m.add_function(wrap_pyfunction!(batch_sma, m)?)?;
869
+ m.add_function(wrap_pyfunction!(equity_sma_crossover, m)?)?;
870
+ m.add_function(wrap_pyfunction!(sweep_sma_crossover, m)?)?;
871
+ m.add_function(wrap_pyfunction!(walk_forward_sma_sweep, m)?)?;
872
+ m.add_function(wrap_pyfunction!(multi_symbol_equity_aggregate, m)?)?;
873
+ m.add_function(wrap_pyfunction!(equity_rsi_mean_reversion, m)?)?;
874
+ m.add_function(wrap_pyfunction!(sweep_rsi_mean_reversion, m)?)?;
875
+ m.add_function(wrap_pyfunction!(ema, m)?)?;
876
+ m.add_function(wrap_pyfunction!(macd, m)?)?;
877
+ m.add_function(wrap_pyfunction!(equity_ema_crossover, m)?)?;
878
+ m.add_function(wrap_pyfunction!(sweep_ema_crossover, m)?)?;
879
+ m.add_function(wrap_pyfunction!(equity_macd_crossover, m)?)?;
880
+ m.add_function(wrap_pyfunction!(sweep_macd, m)?)?;
881
+ Ok(())
882
+ }