diff --git a/.github/workflows/quant.yml b/.github/workflows/quant.yml new file mode 100644 index 00000000..68cd650f --- /dev/null +++ b/.github/workflows/quant.yml @@ -0,0 +1,20 @@ +name: QUANT CI + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Set up Rust + uses: actions/setup-rust@v1 + + - name: Check out code + uses: actions/checkout@v2 + + - name: Test ta_lib + run: cargo test --manifest-path=ta_lib/Cargo.toml diff --git a/.gitignore b/.gitignore index 011102ba..6e8912c3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ __pycache__ .ipynb_checkpoints .DS_Store -.csv \ No newline at end of file +.csv +target \ No newline at end of file diff --git a/ta_lib/Cargo.lock b/ta_lib/Cargo.lock new file mode 100644 index 00000000..daa55d28 --- /dev/null +++ b/ta_lib/Cargo.lock @@ -0,0 +1,14 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "momentum" +version = "0.1.0" +dependencies = [ + "overlap", +] + +[[package]] +name = "overlap" +version = "0.1.0" diff --git a/ta_lib/Cargo.toml b/ta_lib/Cargo.toml new file mode 100644 index 00000000..f07f1793 --- /dev/null +++ b/ta_lib/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +members = [ +"overlap", +"momentum" +] diff --git a/ta_lib/momentum/Cargo.toml b/ta_lib/momentum/Cargo.toml new file mode 100644 index 00000000..da261e3e --- /dev/null +++ b/ta_lib/momentum/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "momentum" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +overlap = { path = "../overlap"} diff --git a/ta_lib/momentum/src/lib.rs b/ta_lib/momentum/src/lib.rs new file mode 100644 index 00000000..09eb9054 --- /dev/null +++ b/ta_lib/momentum/src/lib.rs @@ -0,0 +1 @@ +pub mod rsi; diff --git a/ta_lib/momentum/src/rsi.rs b/ta_lib/momentum/src/rsi.rs new file mode 100644 index 00000000..214e7aef --- /dev/null +++ b/ta_lib/momentum/src/rsi.rs @@ -0,0 +1,90 @@ +use overlap::smma::smma; + +pub fn rsi(data: &[f64], period: usize) -> Vec> { + if data.len() < period { + return vec![None; data.len()]; + } + + let mut gains = vec![0.0; data.len()]; + let mut losses = vec![0.0; data.len()]; + + for i in 1..data.len() { + let change = data[i] - data[i - 1]; + + gains[i] = change.max(0.0); + losses[i] = (-change).max(0.0); + } + + let avg_gain = smma(&gains, period); + let avg_loss = smma(&losses, period); + + let rsi = avg_gain + .iter() + .zip(avg_loss.iter()) + .map(|(&gain, &loss)| match (gain, loss) { + (Some(gain), Some(loss)) if gain + loss > 0.0 => { + let rs = gain / (loss + std::f64::EPSILON); + Some(100.0 - (100.0 / (1.0 + rs))) + } + _ => None, + }) + .collect(); + + rsi +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_rsi_len() { + let data = vec![5.0]; + let result = rsi(&data, 1); + assert_eq!(data.len(), result.len()); + } + + #[test] + fn test_rsi_empty() { + let data = vec![]; + let result = rsi(&data, 14); + assert_eq!(result, vec![]); + } + + #[test] + fn test_rsi_single_value() { + let data = [10.0]; + let rsi_values = rsi(&data, 14); + assert_eq!(rsi_values, vec![None]); + } + + #[test] + fn test_rsi_with_valid_data() { + let data = vec![ + 44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84, + ]; + let result = rsi(&data, 6); + let epsilon = 0.001; + let expected = vec![ + None, + None, + None, + None, + None, + Some(69.602669), + Some(74.642227), + Some(79.480508), + Some(84.221979), + ]; + + for i in 0..result.len() { + match (result[i], expected[i]) { + (Some(a), Some(b)) => { + assert!((a - b).abs() < epsilon, "at position {}: {} != {}", i, a, b) + } + (None, None) => {} + _ => panic!("at position {}: {:?} != {:?}", i, result[i], expected[i]), + } + } + } +} diff --git a/ta_lib/overlap/Cargo.toml b/ta_lib/overlap/Cargo.toml new file mode 100644 index 00000000..e298255f --- /dev/null +++ b/ta_lib/overlap/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "overlap" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/ta_lib/overlap/src/ema.rs b/ta_lib/overlap/src/ema.rs new file mode 100644 index 00000000..f7cec8ea --- /dev/null +++ b/ta_lib/overlap/src/ema.rs @@ -0,0 +1,49 @@ +pub fn ema(data: &[f64], period: usize) -> Vec> { + if data.len() < period { + return vec![None; data.len()]; + } + + let alpha = 2.0 / (period as f64 + 1.0); + + let mut ema = vec![None; data.len()]; + + let mut ema_current = data[0]; + + for i in 1..data.len() { + ema_current = (data[i] - ema_current) * alpha + ema_current; + if i >= period - 1 { + ema[i] = Some(ema_current); + } + } + + ema +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ema_len() { + let data = vec![1.0, 2.0]; + let result = ema(&data, 3); + assert_eq!(data.len(), result.len()); + } + + #[test] + fn test_ema_edge_case() { + let data = vec![1.0, 2.0]; + let result = ema(&data, 3); + assert_eq!(result, vec![None, None]); + } + + #[test] + fn test_ema() { + let data = vec![1.0, 2.0, 3.0, 4.0, 5.0]; + let result = ema(&data, 3); + assert_eq!( + result, + vec![None, None, Some(2.25), Some(3.125), Some(4.0625)] + ); + } +} diff --git a/ta_lib/overlap/src/lib.rs b/ta_lib/overlap/src/lib.rs new file mode 100644 index 00000000..366ad0d4 --- /dev/null +++ b/ta_lib/overlap/src/lib.rs @@ -0,0 +1,3 @@ +pub mod ema; +pub mod sma; +pub mod smma; diff --git a/ta_lib/overlap/src/sma.rs b/ta_lib/overlap/src/sma.rs new file mode 100644 index 00000000..61a445ac --- /dev/null +++ b/ta_lib/overlap/src/sma.rs @@ -0,0 +1,43 @@ +pub fn sma(data: &[f64], period: usize) -> Vec> { + let mut sma = vec![None; data.len()]; + let mut sum = 0.0; + + for i in 0..data.len() { + sum += data[i]; + + if i >= period { + sum -= data[i - period]; + sma[i] = Some(sum / period as f64); + } else if i + 1 == period { + sma[i] = Some(sum / period as f64); + } + } + + sma +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sma_len() { + let data = vec![1.0, 2.0]; + let result = sma(&data, 3); + assert_eq!(data.len(), result.len()); + } + + #[test] + fn test_sma_edge_case() { + let data = vec![1.0, 2.0]; + let result = sma(&data, 3); + assert_eq!(result, vec![None, None]); + } + + #[test] + fn test_sma() { + let data = vec![1.0, 2.0, 3.0, 4.0, 5.0]; + let result = sma(&data, 3); + assert_eq!(result, vec![None, None, Some(2.0), Some(3.0), Some(4.0)]); + } +} diff --git a/ta_lib/overlap/src/smma.rs b/ta_lib/overlap/src/smma.rs new file mode 100644 index 00000000..682fffee --- /dev/null +++ b/ta_lib/overlap/src/smma.rs @@ -0,0 +1,57 @@ +pub fn smma(data: &[f64], period: usize) -> Vec> { + if data.len() < period { + return vec![None; data.len()]; + } + + let alpha = 1.0 / period as f64; + + let mut smma = vec![None; data.len()]; + + let mut smma_current = data[0]; + + for i in 1..data.len() { + smma_current = (data[i] - smma_current) * alpha + smma_current; + if i >= period - 1 { + smma[i] = Some(smma_current); + } + } + + smma +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_smma_len() { + let data = vec![1.0, 2.0]; + let result = smma(&data, 3); + assert_eq!(data.len(), result.len()); + } + + #[test] + fn test_smmma_edge_case() { + let data = vec![1.0, 2.0]; + let result = smma(&data, 3); + assert_eq!(result, vec![None, None]); + } + + #[test] + fn test_smma() { + let data = vec![1.0, 2.0, 3.0, 4.0, 5.0]; + let result = smma(&data, 3); + let expected = vec![None, None, Some(1.888), Some(2.592), Some(3.395)]; + let epsilon = 0.001; + + for i in 0..result.len() { + match (result[i], expected[i]) { + (Some(a), Some(b)) => { + assert!((a - b).abs() < epsilon, "at position {}: {} != {}", i, a, b) + } + (None, None) => {} + _ => panic!("at position {}: {:?} != {:?}", i, result[i], expected[i]), + } + } + } +}