├── pyproject.toml ├── Cargo.toml ├── .gitignore ├── speed_tests ├── sma.py └── ema.py ├── .github └── workflows │ ├── rust.yml │ └── CI.yml ├── LICENSE ├── README.md └── src └── lib.rs /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=0.12,<0.13"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "ZenithTA" 7 | dependencies = [ 8 | "numpy" 9 | ] 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ZenithTA" 3 | version = "1.0.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | name = "ZenithTA" 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | pyo3 = { version = "0.15.1", features = ["abi3-py36", "extension-module"] } 12 | numpy = "0.15" 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | # Added by cargo 14 | # 15 | # already existing elements were commented out 16 | 17 | /target 18 | #Cargo.lock 19 | 20 | # Add for maturin 21 | dist/ 22 | -------------------------------------------------------------------------------- /speed_tests/sma.py: -------------------------------------------------------------------------------- 1 | import pandas_datareader as pdr 2 | import pandas as pd 3 | from ZenithTA import * 4 | from timeit import default_timer as timer 5 | from datetime import timedelta 6 | 7 | data = pdr.get_data_yahoo('NVDA') 8 | 9 | print("Timing ZenithTA:") 10 | a = data['Close'].tolist() 11 | start = timer() 12 | j = sma(a,5) 13 | end = timer() 14 | print(timedelta(seconds=end-start)) 15 | 16 | print("Timing Pandas:") 17 | start = timer() 18 | data['SMA(5)'] = data.Close.rolling(5).mean() 19 | end = timer() 20 | b = data['SMA(5)'].tolist() 21 | print(timedelta(seconds=end-start)) 22 | -------------------------------------------------------------------------------- /speed_tests/ema.py: -------------------------------------------------------------------------------- 1 | import pandas_datareader as pdr 2 | import pandas as pd 3 | from ZenithTA import * 4 | from timeit import default_timer as timer 5 | from datetime import timedelta 6 | 7 | data = pdr.get_data_yahoo('NVDA') 8 | 9 | print("Timing ZenithTA:") 10 | start = timer() 11 | a = data['Close'].tolist() 12 | j = ema(a,4,2.0) 13 | end = timer() 14 | print(j[0:10:1]) 15 | print(timedelta(seconds=end-start)) 16 | 17 | print("Timing Pandas:") 18 | start = timer() 19 | data['4dayEWM'] = data['Close'].ewm(span=4, adjust=False).mean() 20 | b = data['4dayEWM'].tolist() 21 | print(b[0:10:1]) 22 | end = timer() 23 | print(timedelta(seconds=end-start)) -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Cargo Cache 20 | uses: actions/cache@v1 21 | with: 22 | path: ~/.cargo 23 | key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.toml') }} 24 | restore-keys: | 25 | ${{ runner.os }}-cargo-${{ hashFiles('Cargo.toml') }} 26 | ${{ runner.os }}-cargo 27 | 28 | - name: Cargo Target Cache 29 | uses: actions/cache@v1 30 | with: 31 | path: target 32 | key: ${{ runner.os }}-cargo-target-${{ hashFiles('Cargo.toml') }} 33 | restore-keys: | 34 | ${{ runner.os }}-cargo-target-${{ hashFiles('Cargo.toml') }} 35 | ${{ runner.os }}-cargo-target 36 | 37 | - name: Build 38 | run: cargo build --release --verbose 39 | - name: Run tests 40 | run: cargo test --verbose 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Greg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Rust](https://img.shields.io/github/actions/workflow/status/gregyjames/zenithta/rust.yml?style=for-the-badge)](https://github.com/gregyjames/ZenithTA/actions/workflows/rust.yml) 2 | [![PyPI](https://img.shields.io/pypi/v/zenithta?color=%230s0&style=for-the-badge)](https://pypi.org/project/zenithta/) 3 | [![License](https://img.shields.io/github/license/gregyjames/zenithta?color=%230sd&style=for-the-badge)](https://github.com/gregyjames/ZenithTA/blob/main/LICENSE) 4 | [![Count](https://img.shields.io/tokei/lines/github/gregyjames/zenithta?color=%230fs&style=for-the-badge)](https://github.com/gregyjames/ZenithTA/) 5 | 6 | 7 | 8 | # ZenithTA 9 | #### Formerly Panther 10 | A efficient, high-performance python technical analysis library written in Rust using PyO3 and rust-numpy. 11 | 12 | ## Indicators 13 | - ATR 14 | - CMF 15 | - SMA 16 | - EMA 17 | - RSI 18 | - MACD 19 | - ROC 20 | 21 | ## How to install 22 | `pip3 install zenithta` 23 | 24 | ## How to build (Windows) 25 | - Run `cargo build --release` from the main directory. 26 | - Get the generated dll from the target/release directory. 27 | - Rename extension from .dll to .pyd. 28 | - Place .pyd file in the same folder as script. 29 | - Put `from panther import *` in python script. 30 | 31 | ## Speed 32 | On average, I found the Panther calculations of these indicators to be about 9x or 900% faster than the industry standard way of calculating these indicators using Pandas. Don't believe me? Install the library and run the tests in the speed_tests directory to see it for yourself :) 33 | 34 | ## License 35 | MIT License 36 | 37 | Copyright (c) 2022 Greg James 38 | 39 | Permission is hereby granted, free of charge, to any person obtaining a copy 40 | of this software and associated documentation files (the "Software"), to deal 41 | in the Software without restriction, including without limitation the rights 42 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 43 | copies of the Software, and to permit persons to whom the Software is 44 | furnished to do so, subject to the following conditions: 45 | 46 | The above copyright notice and this permission notice shall be included in all 47 | copies or substantial portions of the Software. 48 | 49 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 50 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 51 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 52 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 53 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 54 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 55 | SOFTWARE. 56 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "*" 9 | pull_request: 10 | 11 | jobs: 12 | macos: 13 | runs-on: macos-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-python@v2 17 | with: 18 | python-version: 3.9 19 | architecture: x64 20 | - name: Install Rust toolchain 21 | uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: nightly 24 | profile: minimal 25 | default: true 26 | - name: Build wheels - x86_64 27 | uses: messense/maturin-action@v1 28 | with: 29 | target: x86_64 30 | args: --release --out dist 31 | - name: Install built wheel - x86_64 32 | run: | 33 | pip install numpy 34 | pip install ZenithTA --no-deps --no-index --find-links dist --force-reinstall 35 | python -c 'import ZenithTA' 36 | - name: Build wheels - universal2 37 | uses: messense/maturin-action@v1 38 | with: 39 | args: --release --universal2 --out dist --no-sdist 40 | - name: Install built wheel - universal2 41 | run: | 42 | pip install ZenithTA --no-deps --no-index --find-links dist --force-reinstall 43 | python -c 'import ZenithTA' 44 | - name: Upload wheels 45 | uses: actions/upload-artifact@v2 46 | with: 47 | name: wheels 48 | path: dist 49 | 50 | windows: 51 | runs-on: windows-latest 52 | strategy: 53 | matrix: 54 | target: [x86] 55 | steps: 56 | - uses: actions/checkout@v2 57 | - uses: actions/setup-python@v2 58 | with: 59 | python-version: 3.9 60 | architecture: ${{ matrix.target }} 61 | - name: Install Rust toolchain 62 | uses: actions-rs/toolchain@v1 63 | with: 64 | toolchain: nightly 65 | profile: minimal 66 | default: true 67 | - name: Build wheels 68 | uses: messense/maturin-action@v1 69 | with: 70 | target: ${{ matrix.target }} 71 | args: --release --out dist --no-sdist 72 | - name: Install built wheel 73 | run: | 74 | pip install numpy 75 | pip install ZenithTA --no-deps --no-index --find-links dist --force-reinstall 76 | python -c 'import ZenithTA' 77 | - name: Upload wheels 78 | uses: actions/upload-artifact@v2 79 | with: 80 | name: wheels 81 | path: dist 82 | 83 | linux: 84 | runs-on: ubuntu-latest 85 | strategy: 86 | matrix: 87 | target: [x86_64, i686] 88 | steps: 89 | - uses: actions/checkout@v2 90 | - uses: actions/setup-python@v2 91 | with: 92 | python-version: 3.9 93 | architecture: x64 94 | - name: Build wheels 95 | uses: messense/maturin-action@v1 96 | with: 97 | rust-toolchain: nightly 98 | target: ${{ matrix.target }} 99 | manylinux: auto 100 | args: --release --out dist --no-sdist 101 | - name: Install built wheel 102 | if: matrix.target == 'x86_64' 103 | run: | 104 | pip install numpy 105 | pip install ZenithTA --no-deps --no-index --find-links dist --force-reinstall 106 | python -c 'import ZenithTA' 107 | - name: Upload wheels 108 | uses: actions/upload-artifact@v2 109 | with: 110 | name: wheels 111 | path: dist 112 | 113 | linux-cross: 114 | runs-on: ubuntu-latest 115 | strategy: 116 | matrix: 117 | target: [aarch64, armv7, s390x, ppc64le] 118 | steps: 119 | - uses: actions/checkout@v2 120 | - uses: actions/setup-python@v2 121 | with: 122 | python-version: 3.9 123 | - name: Build wheels 124 | uses: messense/maturin-action@v1 125 | with: 126 | rust-toolchain: nightly 127 | target: ${{ matrix.target }} 128 | manylinux: auto 129 | args: --release --out dist --no-sdist 130 | - uses: uraimo/run-on-arch-action@v2.0.5 131 | name: Install built wheel 132 | if: matrix.target == 'aarch64' 133 | with: 134 | arch: ${{ matrix.target }} 135 | distro: ubuntu20.04 136 | githubToken: ${{ github.token }} 137 | install: | 138 | apt-get update 139 | apt-get install -y --no-install-recommends python3 python3-pip 140 | pip3 install -U pip numpy 141 | run: | 142 | pip3 install ZenithTA --no-deps --no-index --find-links dist/ --force-reinstall 143 | python3 -c 'import ZenithTA' 144 | - name: Upload wheels 145 | uses: actions/upload-artifact@v2 146 | with: 147 | name: wheels 148 | path: dist 149 | 150 | release: 151 | name: Release 152 | runs-on: ubuntu-latest 153 | if: "startsWith(github.ref, 'refs/tags/')" 154 | needs: [ macos, windows, linux, linux-cross ] 155 | steps: 156 | - uses: actions/download-artifact@v3 157 | with: 158 | name: wheels 159 | - name: Publish to PyPi 160 | env: 161 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 162 | uses: PyO3/maturin-action@v1 163 | with: 164 | command: upload 165 | args: --skip-existing * -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use pyo3::wrap_pyfunction; 3 | use numpy::ndarray::prelude::*; 4 | 5 | //Helper functions for indicators 6 | fn sma_helper(price_ndarray: &Array1, period: usize) -> Array1{ 7 | let length = price_ndarray.len() - period +1; 8 | let mut result = Array1::::zeros(length); 9 | 10 | for i in 0..length { 11 | let slice = price_ndarray.slice(s![i..i+period]); 12 | result[i] = slice.sum()/(period as f32); 13 | } 14 | 15 | result 16 | } 17 | 18 | fn ema_helper(price_ndarray: &Array1, period: usize) -> Array1{ 19 | let length = price_ndarray.len() - period +1; 20 | let mut result = Array1::::zeros(length); 21 | result[0] = price_ndarray.slice(s![0..period]).sum(); 22 | for i in 1..length{ 23 | result[i] = result[i-1]+(price_ndarray[i+period-1]-result[i-1])*(2.0/((period as f32)+1.0)); 24 | } 25 | 26 | result 27 | } 28 | 29 | //Indicator Python function wrappers 30 | #[pyfunction] 31 | fn sma(price: Vec, period: usize) -> PyResult> { 32 | let price_ndarray = Array::from_vec(price); 33 | let length = price_ndarray.len() - period +1; 34 | let mut result = Array1::::zeros(length); 35 | 36 | for i in 0..length { 37 | let slice = price_ndarray.slice(s![i..i+period]); 38 | result[i] = slice.sum()/(period as f32); 39 | } 40 | 41 | Ok(Array::to_vec(&result)) 42 | } 43 | 44 | #[pyfunction] 45 | fn ema(price: Vec, period: usize, smoothing: f32) -> PyResult> { 46 | let price_ndarray = Array::from_vec(price); 47 | let length = price_ndarray.len(); 48 | let mut result = Array1::::zeros(length); 49 | 50 | let weight = smoothing / (period + 1) as f32; 51 | result[0] = price_ndarray[0]; 52 | 53 | for i in 1..length { 54 | result[i] = (price_ndarray[i]*weight) + (result[i-1] * (1.0-weight)); 55 | } 56 | 57 | Ok(Array::to_vec(&result)) 58 | } 59 | 60 | #[pyfunction] 61 | fn rsi(price: Vec, period: usize) -> PyResult>{ 62 | let price_ndarray = Array::from_vec(price); 63 | let mut change = Array1::::zeros(price_ndarray.len()); 64 | let mut gain = Array1::::zeros(price_ndarray.len()); 65 | let mut loss = Array1::::zeros(price_ndarray.len()); 66 | let mut ag = Array1::::zeros(price_ndarray.len()); 67 | let mut al = Array1::::zeros(price_ndarray.len()); 68 | let mut result = Array1::::zeros(price_ndarray.len()); 69 | for i in 1..price_ndarray.len(){ 70 | change[i] = price_ndarray[i] - price_ndarray[i-1]; 71 | if change[i] == 0.0{ 72 | gain[i] = 0.0; 73 | loss[i] = 0.0; 74 | } else if change[i]<0.0{ 75 | gain[i] = 0.0; 76 | loss[i] = (change[i]).abs(); 77 | }else{ 78 | gain[i] = change[i]; 79 | loss[i] = 0.0; 80 | } 81 | } 82 | ag[period] = gain.slice(s![1..period+1]).sum()/(period as f32); 83 | al[period] = loss.slice(s![1..period+1]).sum()/(period as f32); 84 | 85 | for i in period+1..price_ndarray.len(){ 86 | ag[i] = (ag[i-1]*(period as f32-1.0)+gain[i])/period as f32; 87 | al[i] = (al[i-1]*(period as f32-1.0)+loss[i])/period as f32; 88 | } 89 | for i in period+1..price_ndarray.len(){ 90 | result[i] = 100.0-(100.0/(1.0+ag[i]/al[i])) 91 | } 92 | 93 | //result.slice(s![period..]).to_owned() 94 | Ok(Array::to_vec(&(result.slice(s![period..]).to_owned()))) 95 | } 96 | 97 | #[pyfunction] 98 | fn macd(price: Vec, period_fast: usize, period_slow: usize, period_signal: usize) -> PyResult<(Vec,Vec)>{ 99 | let price_ndarray = Array::from_vec(price); 100 | let line = ema_helper(&price_ndarray, period_fast).slice(s![period_slow-period_fast..]).to_owned() - ema_helper(&price_ndarray,period_slow); 101 | let signal = ema_helper(&price_ndarray, period_signal); 102 | 103 | Ok((Array::to_vec(&line), Array::to_vec(&signal))) 104 | } 105 | 106 | #[pyfunction] 107 | fn roc(price: Vec, period: usize) -> PyResult> { 108 | let price_ndarray = Array::from_vec(price); 109 | let length = price_ndarray.len() - period; 110 | let mut result = Array1::::zeros(length); 111 | 112 | for i in period..price_ndarray.len() { 113 | result[i-period] = ((price_ndarray[i]-price_ndarray[i-period])/price_ndarray[i-period])*100.0; 114 | } 115 | 116 | Ok(Array::to_vec(&result)) 117 | } 118 | 119 | #[pyfunction] 120 | fn atr(high: Vec, low: Vec, close: Vec, period: usize) -> PyResult> { 121 | let length = high.len(); 122 | let high_ndarray = Array::from_vec(high); 123 | let low_ndarray = Array::from_vec(low); 124 | let close_ndarray = Array::from_vec(close); 125 | let mut tr = Array1::::zeros(length); 126 | let mut result = Array1::::zeros(length - period + 1); 127 | 128 | tr[0] = high_ndarray[0]-low_ndarray[0]; 129 | for i in 1..high_ndarray.len() { 130 | let hl = high_ndarray[i] - low_ndarray[i]; 131 | let hpc = (high_ndarray[i] - close_ndarray[i-1]).abs(); 132 | let lpc = (low_ndarray[i] - close_ndarray[i-1]).abs(); 133 | tr[i] = f32::max(f32::max(hl, hpc), lpc); 134 | } 135 | 136 | 137 | result[0] = tr.slice(s![0..period]).sum()/period as f32; 138 | 139 | for i in 1.. length -period+1 { 140 | result[i] = (result[i-1]*(period as f32-1.0)+tr[i+period-1])/period as f32; 141 | } 142 | Ok(Array::to_vec(&result)) 143 | } 144 | 145 | #[pyfunction] 146 | fn cmf(high: Vec, low: Vec, close: Vec, volume: Vec, period: usize) -> PyResult> { 147 | let length = high.len(); 148 | let high_ndarray = Array::from_vec(high); 149 | let low_ndarray = Array::from_vec(low); 150 | let close_ndarray = Array::from_vec(close); 151 | let volume_ndarray = Array::from_vec(volume); 152 | let mut result = Array1::::zeros(length - period + 1); 153 | 154 | let a = (&close_ndarray)-(&low_ndarray); 155 | let b = (&high_ndarray)-(&close_ndarray); 156 | let c = (&high_ndarray)-(&low_ndarray); 157 | 158 | let mfv = ((a-b)/c)*period as f32; 159 | for i in 0..length-period+1 { 160 | result[i] = mfv.slice(s![i..i+period]).sum()/ volume_ndarray.slice(s![i..i+period]).sum(); 161 | } 162 | 163 | Ok(Array::to_vec(&result)) 164 | } 165 | 166 | #[pymodule] 167 | fn ZenithTA(_py: Python, m: &PyModule) -> PyResult<()> { 168 | m.add_function(wrap_pyfunction!(sma, m)?)?; 169 | m.add_function(wrap_pyfunction!(cmf, m)?)?; 170 | m.add_function(wrap_pyfunction!(atr, m)?)?; 171 | m.add_function(wrap_pyfunction!(ema, m)?)?; 172 | m.add_function(wrap_pyfunction!(rsi, m)?)?; 173 | m.add_function(wrap_pyfunction!(roc, m)?)?; 174 | m.add_function(wrap_pyfunction!(macd, m)?)?; 175 | Ok(()) 176 | } 177 | 178 | --------------------------------------------------------------------------------