├── .deepsource.toml ├── .devcontainer └── devcontainer.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── ci.yml │ └── cla.yml ├── .gitignore ├── .gomarkdoc.yml ├── CLA.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── asset ├── README.md ├── asset.go ├── file_system_repository.go ├── file_system_repository_test.go ├── in_memory_repository.go ├── in_memory_repository_test.go ├── repository.go ├── repository_factory.go ├── repository_factory_test.go ├── snapshot.go ├── snapshot_test.go ├── sql_repository.go ├── sql_repository_dialect.go ├── sync.go ├── sync_test.go ├── testdata │ ├── empty.csv │ ├── repository │ │ └── brk-b.csv │ └── since.csv ├── tiingo_repository.go └── tiingo_repository_test.go ├── backtest ├── README.md ├── backtest.go ├── backtest_test.go ├── data_report.go ├── data_report_test.go ├── data_strategy_result.go ├── html_asset_report.tmpl ├── html_report.go ├── html_report.tmpl ├── report.go ├── report_factory.go ├── report_factory_test.go └── testdata │ └── repository │ └── brk-b.csv ├── cmd ├── indicator-backtest │ └── main.go └── indicator-sync │ └── main.go ├── codecov.yml ├── go.mod ├── helper ├── README.md ├── abs.go ├── abs_test.go ├── add.go ├── add_test.go ├── annotation_report_column.go ├── apply.go ├── apply_test.go ├── bst.go ├── bst_test.go ├── buffered.go ├── buffered_test.go ├── chan_to_json.go ├── chan_to_json_test.go ├── chan_to_slice.go ├── chan_to_slice_test.go ├── change.go ├── change_percent.go ├── change_percent_test.go ├── change_ratio.go ├── change_ratio_test.go ├── change_test.go ├── check.go ├── check_test.go ├── closer.go ├── closer_test.go ├── count.go ├── count_test.go ├── csv.go ├── csv_test.go ├── database.go ├── days_between.go ├── days_between_test.go ├── decrement_by.go ├── decrement_by_test.go ├── divide.go ├── divide_by.go ├── divide_by_test.go ├── divide_test.go ├── drain.go ├── drain_test.go ├── duplicate.go ├── duplicate_test.go ├── echo.go ├── echo_test.go ├── field.go ├── field_test.go ├── filter.go ├── filter_test.go ├── first.go ├── first_test.go ├── gcd.go ├── gcd_test.go ├── head.go ├── head_test.go ├── helper.go ├── increment_by.go ├── increment_by_test.go ├── json_to_chan.go ├── json_to_chan_test.go ├── keep_negatives.go ├── keep_negatives_test.go ├── keep_positives.go ├── keep_positives_test.go ├── last.go ├── last_test.go ├── lcm.go ├── lcm_test.go ├── map.go ├── map_test.go ├── map_with_previous.go ├── map_with_previous_test.go ├── multiply.go ├── multiply_by.go ├── multiply_by_test.go ├── multiply_test.go ├── numeric_report_column.go ├── operate.go ├── operate3.go ├── operate3_test.go ├── operate_test.go ├── pipe.go ├── pipe_test.go ├── pow.go ├── pow_test.go ├── reflect.go ├── reflect_test.go ├── remove.go ├── report.go ├── report.tmpl ├── report_test.go ├── ring.go ├── ring_test.go ├── round_digit.go ├── round_digit_test.go ├── round_digits.go ├── round_digits_test.go ├── seq.go ├── seq_test.go ├── shift.go ├── shift_test.go ├── sign.go ├── sign_test.go ├── since.go ├── since_test.go ├── skip.go ├── skip_test.go ├── slice_to_chan.go ├── slice_to_chan_test.go ├── sqrt.go ├── sqrt_test.go ├── subtract.go ├── subtract_test.go ├── sync.go ├── sync_test.go ├── testdata │ ├── report.csv │ ├── with_header.csv │ └── without_header.csv ├── waitable.go └── waitable_test.go ├── logo.png ├── momentum ├── README.md ├── awesome_oscillator.go ├── awesome_oscillator_test.go ├── chaikin_oscillator.go ├── chaikin_oscillator_test.go ├── ichimoku_cloud.go ├── ichimoku_cloud_test.go ├── momentum.go ├── ppo.go ├── ppo_test.go ├── pvo.go ├── pvo_test.go ├── qstick.go ├── qstick_test.go ├── rsi.go ├── rsi_test.go ├── stochastic_oscillator.go ├── stochastic_oscillator_test.go ├── stochastic_rsi.go ├── stochastic_rsi_test.go ├── testdata │ ├── awesome_oscillator.csv │ ├── chaikin_oscillator.csv │ ├── ichimoku_cloud.csv │ ├── ppo.csv │ ├── pvo.csv │ ├── qstick.csv │ ├── rsi.csv │ ├── stochastic_oscillator.csv │ ├── stochastic_rsi.csv │ └── williams_r.csv ├── williams_r.go └── williams_r_test.go ├── revive.toml ├── sponsors.svg ├── strategy ├── README.md ├── action.go ├── action_test.go ├── and_strategy.go ├── and_strategy_test.go ├── buy_and_hold_strategy.go ├── buy_and_hold_strategy_test.go ├── compound │ ├── README.md │ ├── compound.go │ ├── macd_rsi_strategy.go │ ├── macd_rsi_strategy_test.go │ └── testdata │ │ ├── brk-b.csv │ │ └── macd_rsi_strategy.csv ├── decorator │ ├── README.md │ ├── decorator.go │ ├── inverse_strategy.go │ ├── inverse_strategy_test.go │ ├── no_loss_strategy.go │ ├── no_loss_strategy_test.go │ ├── stop_loss_strategy.go │ ├── stop_loss_strategy_test.go │ └── testdata │ │ ├── brk-b.csv │ │ ├── inverse_strategy.csv │ │ ├── no_loss_strategy.csv │ │ └── stop_loss_strategy.csv ├── majority_strategy.go ├── majority_strategy_test.go ├── momentum │ ├── README.md │ ├── awesome_oscillator_strategy.go │ ├── awesome_oscillator_strategy_test.go │ ├── momentum.go │ ├── rsi_strategy.go │ ├── rsi_strategy_test.go │ ├── stochastic_rsi_strategy.go │ ├── stochastic_rsi_strategy_test.go │ ├── testdata │ │ ├── awesome_oscillator_strategy.csv │ │ ├── brk-b.csv │ │ ├── rsi_strategy.csv │ │ ├── stochastic_rsi_strategy.csv │ │ └── triple_rsi_strategy.csv │ ├── triple_rsi_strategy.go │ └── triple_rsi_strategy_test.go ├── or_strategy.go ├── or_strategy_test.go ├── outcome.go ├── outcome_test.go ├── result.go ├── split_strategy.go ├── split_strategy_test.go ├── strategy.go ├── testdata │ ├── and.csv │ ├── buy_and_hold_strategy.csv │ ├── majority.csv │ ├── or.csv │ ├── repository │ │ └── brk-b.csv │ ├── split.csv │ └── x ├── trend │ ├── README.md │ ├── alligator_strategy.go │ ├── alligator_strategy_test.go │ ├── apo_strategy.go │ ├── apo_strategy_test.go │ ├── aroon_strategy.go │ ├── aroon_strategy_test.go │ ├── bop_strategy.go │ ├── bop_strategy_test.go │ ├── cci_strategy.go │ ├── cci_strategy_test.go │ ├── dema_strategy.go │ ├── dema_strategy_test.go │ ├── envelope_strategy.go │ ├── envelope_strategy_test.go │ ├── golden_cross_strategy.go │ ├── golden_cross_strategy_test.go │ ├── kama_strategy.go │ ├── kama_strategy_test.go │ ├── kdj_strategy.go │ ├── kdj_strategy_test.go │ ├── macd_strategy.go │ ├── macd_strategy_test.go │ ├── qstick_strategy.go │ ├── qstick_strategy_test.go │ ├── smma_strategy.go │ ├── smma_strategy_test.go │ ├── testdata │ │ ├── alligator_strategy.csv │ │ ├── apo_strategy.csv │ │ ├── aroon_strategy.csv │ │ ├── bop_strategy.csv │ │ ├── brk-b.csv │ │ ├── cci_strategy.csv │ │ ├── dema_strategy.csv │ │ ├── envelope_strategy.csv │ │ ├── golden_cross_strategy.csv │ │ ├── kama_strategy.csv │ │ ├── kdj_strategy.csv │ │ ├── macd_strategy.csv │ │ ├── qstick_strategy.csv │ │ ├── smma_strategy.csv │ │ ├── trima_strategy.csv │ │ ├── triple_moving_average_crossover_strategy.csv │ │ ├── trix_strategy.csv │ │ ├── tsi_strategy.csv │ │ ├── vwma_strategy.csv │ │ └── weighted_close_strategy.csv │ ├── trend.go │ ├── trima_strategy.go │ ├── trima_strategy_test.go │ ├── triple_moving_average_crossover_strategy.go │ ├── triple_moving_average_crossover_strategy_test.go │ ├── trix_strategy.go │ ├── trix_strategy_test.go │ ├── tsi_strategy.go │ ├── tsi_strategy_test.go │ ├── vwma_strategy.go │ ├── vwma_strategy_test.go │ ├── weighted_close_strategy.go │ └── weighted_close_strategy_test.go ├── volatility │ ├── README.md │ ├── bollinger_bands_strategy.go │ ├── bollinger_bands_strategy_test.go │ ├── super_trend_strategy.go │ ├── super_trend_strategy_test.go │ ├── testdata │ │ ├── bollinger_bands_strategy.csv │ │ ├── brk-b.csv │ │ └── super_trend_strategy.csv │ └── volatility.go └── volume │ ├── README.md │ ├── chaikin_money_flow_strategy.go │ ├── chaikin_money_flow_strategy_test.go │ ├── ease_of_movement_strategy.go │ ├── ease_of_movement_strategy_test.go │ ├── force_index_strategy.go │ ├── force_index_strategy_test.go │ ├── money_flow_index_strategy.go │ ├── money_flow_index_strategy_test.go │ ├── negative_volume_index_strategy.go │ ├── negative_volume_index_strategy_test.go │ ├── testdata │ ├── brk-b.csv │ ├── chaikin_money_flow_strategy.csv │ ├── ease_of_movement_strategy.csv │ ├── force_index_strategy.csv │ ├── money_flow_index_strategy.csv │ ├── negative_volume_index_strategy.csv │ └── volume_weighted_average_price_strategy.csv │ ├── volume.go │ ├── weighted_average_price_strategy.go │ └── weighted_average_price_strategy_test.go ├── taskfile.yml ├── trend ├── README.md ├── apo.go ├── apo_test.go ├── aroon.go ├── aroon_test.go ├── bop.go ├── bop_test.go ├── cci.go ├── cci_test.go ├── dema.go ├── dema_test.go ├── ema.go ├── ema_test.go ├── envelope.go ├── envelope_test.go ├── hma.go ├── hma_test.go ├── kama.go ├── kama_test.go ├── kdj.go ├── kdj_test.go ├── ma.go ├── macd.go ├── macd_test.go ├── mass_index.go ├── mass_index_test.go ├── mlr.go ├── mlr_test.go ├── mls.go ├── mls_test.go ├── moving_max.go ├── moving_max_test.go ├── moving_min.go ├── moving_min_test.go ├── moving_sum.go ├── moving_sum_test.go ├── rma.go ├── rma_test.go ├── sma.go ├── sma_test.go ├── smma.go ├── smma_test.go ├── tema.go ├── tema_test.go ├── testdata │ ├── apo.csv │ ├── aroon.csv │ ├── bop.csv │ ├── cci.csv │ ├── envelope_ema.csv │ ├── envelope_sma.csv │ ├── hma.csv │ ├── kama.csv │ ├── kdj.csv │ ├── macd.csv │ ├── mass_index.csv │ ├── mlr.csv │ ├── mls.csv │ ├── rma.csv │ ├── smma.csv │ ├── tema.csv │ ├── trima_even.csv │ ├── trima_odd.csv │ ├── trix.csv │ ├── tsi.csv │ ├── typical_price.csv │ ├── vwma.csv │ ├── weighted_close.csv │ └── wma.csv ├── trend.go ├── trima.go ├── trima_test.go ├── trix.go ├── trix_test.go ├── tsi.go ├── tsi_test.go ├── typical_price.go ├── typical_price_test.go ├── vwma.go ├── vwma_test.go ├── weighted_close.go ├── weighted_close_test.go ├── wma.go └── wma_test.go ├── volatility ├── README.md ├── acceleration_bands.go ├── acceleration_bands_test.go ├── atr.go ├── atr_test.go ├── bollinger_band_width.go ├── bollinger_band_width_test.go ├── bollinger_bands.go ├── bollinger_bands_test.go ├── chandelier_exit.go ├── chandelier_exit_test.go ├── donchian_channel.go ├── donchian_channel_test.go ├── keltner_channel.go ├── keltner_channel_test.go ├── moving_std.go ├── moving_std_test.go ├── percent_b.go ├── percent_b_test.go ├── po.go ├── po_test.go ├── super_trend.go ├── super_trend_test.go ├── testdata │ ├── acceleration_bands.csv │ ├── atr.csv │ ├── bollinger_band_width.csv │ ├── bollinger_bands.csv │ ├── chandelier_exit.csv │ ├── donchian_channel.csv │ ├── keltner_channel.csv │ ├── moving_std.csv │ ├── percent_b.csv │ ├── po.csv │ ├── super_trend.csv │ └── ulcer_index.csv ├── ulcer_index.go ├── ulcer_index_test.go └── volatility.go └── volume ├── README.md ├── ad.go ├── ad_test.go ├── cmf.go ├── cmf_test.go ├── emv.go ├── emv_test.go ├── fi.go ├── fi_test.go ├── mfi.go ├── mfi_test.go ├── mfm.go ├── mfm_test.go ├── mfv.go ├── mfv_test.go ├── nvi.go ├── nvi_test.go ├── obv.go ├── obv_test.go ├── testdata ├── ad.csv ├── cmf.csv ├── emv.csv ├── fi.csv ├── mfi.csv ├── mfm.csv ├── mfv.csv ├── nvi.csv ├── obv.csv ├── vpt.csv └── vwap.csv ├── volume.go ├── vpt.go ├── vpt_test.go ├── vwap.go └── vwap_test.go /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "go" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | import_root = "github.com/cinar/indicator" -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/go 3 | { 4 | "name": "Go", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/go:1-1.22-bullseye", 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | // "forwardPorts": [], 13 | 14 | // Use 'postCreateCommand' to run commands after the container is created. 15 | // "postCreateCommand": "go version", 16 | 17 | // Configure tool-specific properties. 18 | "customizations": { 19 | "vscode": { 20 | "settings": { 21 | "go.lintTool": "revive", 22 | "go.lintOnSave": "package" 23 | }, 24 | "extensions": [ 25 | "github.vscode-pull-request-github" 26 | ] 27 | } 28 | } 29 | 30 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 31 | // "remoteUser": "root" 32 | } 33 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: [ cinar ] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Describe Request 2 | 3 | Please describe your request. 4 | 5 | Fixed # (issue) 6 | 7 | # Change Type 8 | 9 | What is the type of this change. 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version-file: ./go.mod 19 | 20 | - name: Setup cache 21 | uses: actions/cache@v4 22 | with: 23 | path: | 24 | ~/.cache/go-build 25 | ~/go/pkg/mod 26 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 27 | restore-keys: | 28 | ${{ runner.os }}-go- 29 | 30 | - name: Run task 31 | run: go run github.com/go-task/task/v3/cmd/task@v3.38.0 32 | 33 | - name: Upload coverage reports to Codecov 34 | uses: codecov/codecov-action@v4.5.0 35 | with: 36 | token: ${{ secrets.CODECOV_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/cla.yml: -------------------------------------------------------------------------------- 1 | name: "CLA Assistant" 2 | on: 3 | issue_comment: 4 | types: [created] 5 | pull_request_target: 6 | types: [opened,closed,synchronize] 7 | 8 | permissions: 9 | actions: write 10 | contents: write 11 | pull-requests: write 12 | statuses: write 13 | 14 | jobs: 15 | CLAAssistant: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: "CLA Assistant" 19 | if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' 20 | uses: contributor-assistant/github-action@v2.5.1 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | branch: 'cla-signatures' 25 | path-to-signatures: 'signatures/version1/cla.json' 26 | path-to-document: 'https://github.com/cinar/indicator/blob/master/CLA.md' # e.g. a CLA or a DCO document 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | indicator-backtest 11 | indicator-sync 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | # vendor/ 21 | 22 | # Go workspace file 23 | go.work 24 | 25 | # GoLand project 26 | .idea 27 | 28 | -------------------------------------------------------------------------------- /.gomarkdoc.yml: -------------------------------------------------------------------------------- 1 | excludeDirs: "./cmd/..." 2 | output: "{{.Dir}}/README.md" 3 | repository: 4 | url: https://github.com/cinar/indicator 5 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 2.x.x | :white_check_mark: | 8 | | 1.x.x | :white_check_mark: | 9 | | < 1.0 | :x: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | If you found a vulnerability with this library, please file a new issue. -------------------------------------------------------------------------------- /asset/asset.go: -------------------------------------------------------------------------------- 1 | // Package asset contains the asset related functions. 2 | // 3 | // This package belongs to the Indicator project. Indicator is 4 | // a Golang module that supplies a variety of technical 5 | // indicators, strategies, and a backtesting framework 6 | // for analysis. 7 | // 8 | // # License 9 | // 10 | // Copyright (c) 2021-2024 Onur Cinar. 11 | // The source code is provided under GNU AGPLv3 License. 12 | // https://github.com/cinar/indicator 13 | // 14 | // # Disclaimer 15 | // 16 | // The information provided on this project is strictly for 17 | // informational purposes and is not to be construed as 18 | // advice or solicitation to buy or sell any security. 19 | package asset 20 | -------------------------------------------------------------------------------- /asset/repository.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package asset 6 | 7 | import ( 8 | "errors" 9 | "time" 10 | ) 11 | 12 | // ErrRepositoryAssetNotFound indicates that the given asset name is not found in the repository. 13 | var ErrRepositoryAssetNotFound = errors.New("asset is not found") 14 | 15 | // ErrRepositoryAssetEmpty indicates that the given asset has no snapshots. 16 | var ErrRepositoryAssetEmpty = errors.New("asset empty") 17 | 18 | // Repository serves as a centralized storage and retrieval 19 | // location for asset snapshots. 20 | type Repository interface { 21 | // Assets returns the names of all assets in the repository. 22 | Assets() ([]string, error) 23 | 24 | // Get attempts to return a channel of snapshots for 25 | // the asset with the given name. 26 | Get(name string) (<-chan *Snapshot, error) 27 | 28 | // GetSince attempts to return a channel of snapshots for 29 | // the asset with the given name since the given date. 30 | GetSince(name string, date time.Time) (<-chan *Snapshot, error) 31 | 32 | // LastDate returns the date of the last snapshot for 33 | // the asset with the given name. 34 | LastDate(name string) (time.Time, error) 35 | 36 | // Append adds the given snapshows to the asset with the 37 | // given name. 38 | Append(name string, snapshots <-chan *Snapshot) error 39 | } 40 | -------------------------------------------------------------------------------- /asset/sql_repository_dialect.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package asset 6 | 7 | // SQLRepositoryDialect defines the SQL dialect for the SQL repository. 8 | type SQLRepositoryDialect interface { 9 | // CreateTable returns the SQL statement to create the repository table. 10 | CreateTable() string 11 | 12 | // DropTable returns the SQL statement to drop the repository table. 13 | DropTable() string 14 | 15 | // Assets returns the SQL statement to get the names of all assets in the respository. 16 | Assets() string 17 | 18 | // GetSince returns the SQL statement to query snapshots for the asset with the given name since the given date. 19 | GetSince() string 20 | 21 | // LastDate returns the SQL statement to query for the last date for the asset with the given name. 22 | LastDate() string 23 | 24 | // Appends returns the SQL statement to add the given snapshots to the asset with the given name. 25 | Append() string 26 | } 27 | -------------------------------------------------------------------------------- /asset/testdata/empty.csv: -------------------------------------------------------------------------------- 1 | Date,Open,High,Low,Close,Adj Close,Volume 2 | -------------------------------------------------------------------------------- /asset/testdata/since.csv: -------------------------------------------------------------------------------- 1 | Date,Open,High,Low,Close,Adj Close,Volume 2 | 2023-11-01,341.209991,345.329987,340.579987,343.75,343.75,2789700 3 | 2023-11-02,346.390015,349.390015,344.5,349.019989,349.019989,3433700 4 | 2023-11-03,350.170013,354.350006,349.790009,351.809998,351.809998,4409100 5 | 2023-11-06,354.029999,354.029999,344.059998,346.630005,346.630005,5486200 6 | 2023-11-07,346.809998,346.950012,344.299988,346.170013,346.170013,3062900 7 | 2023-11-08,346.850006,348,344.690002,346.299988,346.299988,2602400 8 | 2023-11-09,347.640015,350.109985,346.880005,348.179993,348.179993,3052100 9 | 2023-11-10,349.600006,351.200012,348.600006,350.559998,350.559998,3701100 10 | 2023-11-13,350.089996,350.649994,348.809998,350.01001,350.01001,2196200 11 | 2023-11-14,352.519989,355.950012,351.25,354.25,354.25,3387500 12 | 2023-11-15,355.019989,357.309998,354.480011,356.790009,356.790009,3572900 13 | 2023-11-16,357.790009,360,357.230011,359.859985,359.859985,2822500 14 | 2023-11-17,360.470001,360.559998,358.070007,358.929993,358.929993,3260000 15 | 2023-11-20,359.350006,362.609985,358.179993,361.329987,361.329987,3215300 16 | 2023-11-21,360.579987,363.029999,360.25,361,361,2918800 17 | 2023-11-22,361.76001,362.459991,360.049988,361.799988,361.799988,2110200 18 | 2023-11-24,362.51001,363.190002,361.23999,362.679993,362.679993,1282000 19 | 2023-11-27,362.640015,362.640015,359.579987,361.339996,361.339996,2580300 20 | 2023-11-28,361.549988,362.119995,359.209991,360.049988,360.049988,2953500 21 | 2023-11-29,360.950012,361.519989,358.299988,358.690002,358.690002,3141100 22 | -------------------------------------------------------------------------------- /backtest/data_strategy_result.go: -------------------------------------------------------------------------------- 1 | package backtest 2 | -------------------------------------------------------------------------------- /backtest/report.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package backtest 6 | 7 | import ( 8 | "github.com/cinar/indicator/v2/asset" 9 | "github.com/cinar/indicator/v2/strategy" 10 | ) 11 | 12 | // Report is the backtest report interface. 13 | type Report interface { 14 | // Begin is called when the backtest begins. 15 | Begin(assetNames []string, strategies []strategy.Strategy) error 16 | 17 | // AssetBegin is called when backtesting for the given asset begins. 18 | AssetBegin(name string, strategies []strategy.Strategy) error 19 | 20 | // Write writes the given strategy actions and outomes to the report. 21 | Write(assetName string, currentStrategy strategy.Strategy, snapshots <-chan *asset.Snapshot, actions <-chan strategy.Action, outcomes <-chan float64) error 22 | 23 | // AssetEnd is called when backtesting for the given asset ends. 24 | AssetEnd(name string) error 25 | 26 | // End is called when the backtest ends. 27 | End() error 28 | } 29 | -------------------------------------------------------------------------------- /backtest/report_factory.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package backtest 6 | 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | const ( 12 | // HTMLReportBuilderName is the name for the HTML report builder. 13 | HTMLReportBuilderName = "html" 14 | ) 15 | 16 | // ReportBuilderFunc defines a function to build a new report using the given configuration parameter. 17 | type ReportBuilderFunc func(config string) (Report, error) 18 | 19 | // reportBuilders provides mapping for the report builders. 20 | var reportBuilders = map[string]ReportBuilderFunc{ 21 | HTMLReportBuilderName: htmlReportBuilder, 22 | } 23 | 24 | // RegisterReportBuilder registers the given builder. 25 | func RegisterReportBuilder(name string, builder ReportBuilderFunc) { 26 | reportBuilders[name] = builder 27 | } 28 | 29 | // NewReport builds a new report by the given name type and the configuration. 30 | func NewReport(name, config string) (Report, error) { 31 | builder, ok := reportBuilders[name] 32 | if !ok { 33 | return nil, fmt.Errorf("unknown report: %s", name) 34 | } 35 | 36 | return builder(config) 37 | } 38 | 39 | // htmlReportBuilder builds a new HTML report instance. 40 | func htmlReportBuilder(config string) (Report, error) { 41 | return NewHTMLReport(config), nil 42 | } 43 | -------------------------------------------------------------------------------- /backtest/report_factory_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package backtest_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/backtest" 11 | ) 12 | 13 | func TestNewReportUnknown(t *testing.T) { 14 | report, err := backtest.NewReport("unknown", "") 15 | if err == nil { 16 | t.Fatalf("unknown report: %T", report) 17 | } 18 | } 19 | 20 | func TestRegisterReportBuilder(t *testing.T) { 21 | builderName := "testbuilder" 22 | 23 | report, err := backtest.NewReport(builderName, "") 24 | if err == nil { 25 | t.Fatalf("testbuilder is: %T", report) 26 | } 27 | 28 | backtest.RegisterReportBuilder(builderName, func(config string) (backtest.Report, error) { 29 | return backtest.NewHTMLReport(config), nil 30 | }) 31 | 32 | report, err = backtest.NewReport(builderName, "") 33 | if err != nil { 34 | t.Fatalf("testbuilder is not found: %v", err) 35 | } 36 | 37 | _, ok := report.(*backtest.HTMLReport) 38 | if !ok { 39 | t.Fatalf("testbuilder is: %T", report) 40 | } 41 | } 42 | 43 | func TestNewReportMemory(t *testing.T) { 44 | report, err := backtest.NewReport(backtest.HTMLReportBuilderName, "") 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | _, ok := report.(*backtest.HTMLReport) 50 | if !ok { 51 | t.Fatalf("report not correct type: %T", report) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /backtest/testdata/repository/brk-b.csv: -------------------------------------------------------------------------------- 1 | ../../../asset/testdata/repository/brk-b.csv -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 100% 6 | threshold: 0% -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cinar/indicator/v2 2 | 3 | go 1.22 4 | -------------------------------------------------------------------------------- /helper/abs.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | import "math" 8 | 9 | // Abs calculates the absolute value of each value in a channel of float64. 10 | // 11 | // Example: 12 | // 13 | // abs := helper.Abs(helper.SliceToChan([]int{-10, 20, -4, -5})) 14 | // fmt.Println(helper.ChanToSlice(abs)) // [10, 20, 4, 5] 15 | func Abs[T Number](c <-chan T) <-chan T { 16 | return Apply(c, func(n T) T { 17 | return T(math.Abs(float64(n))) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /helper/abs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestAbs(t *testing.T) { 14 | input := helper.SliceToChan([]int{-10, 20, -4, -5}) 15 | expected := helper.SliceToChan([]int{10, 20, 4, 5}) 16 | 17 | actual := helper.Abs(input) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/add.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Add adds each pair of values from the two input channels of float64 8 | // and returns a new channel containing the sums. 9 | // 10 | // Example: 11 | // 12 | // ac := helper.SliceToChan([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) 13 | // bc := helper.SliceToChan([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) 14 | // 15 | // actual := helper.ChanToSlice(helper.Add(ac, bc)) 16 | // 17 | // fmt.Println(actual) // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20] 18 | func Add[T Number](ac, bc <-chan T) <-chan T { 19 | return Operate(ac, bc, func(a, b T) T { 20 | return a + b 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /helper/add_test.go: -------------------------------------------------------------------------------- 1 | // Package helper contains the helper_test functions test. 2 | // 3 | // Copyright (c) 2021-2024 Onur Cinar. 4 | // The source code is provided under GNU AGPLv3 License. 5 | // https://github.com/cinar/indicator 6 | package helper_test 7 | 8 | import ( 9 | "testing" 10 | 11 | "github.com/cinar/indicator/v2/helper" 12 | ) 13 | 14 | func TestAdd(t *testing.T) { 15 | ac := helper.SliceToChan([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) 16 | bc := helper.SliceToChan([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) 17 | 18 | expected := helper.SliceToChan([]int{2, 4, 6, 8, 10, 12, 14, 16, 18, 20}) 19 | 20 | actual := helper.Add(ac, bc) 21 | 22 | err := helper.CheckEquals(actual, expected) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /helper/annotation_report_column.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | import "fmt" 8 | 9 | // annotationReportColumn is the annotation report column struct. 10 | type annotationReportColumn struct { 11 | ReportColumn 12 | values <-chan string 13 | } 14 | 15 | // NewAnnotationReportColumn returns a new instance of an annotation column for a report. 16 | func NewAnnotationReportColumn(values <-chan string) ReportColumn { 17 | return &annotationReportColumn{ 18 | values: values, 19 | } 20 | } 21 | 22 | // Name returns the name of the report column. 23 | func (*annotationReportColumn) Name() string { 24 | return "" 25 | } 26 | 27 | // Type returns number as the data type. 28 | func (*annotationReportColumn) Type() string { 29 | return "string" 30 | } 31 | 32 | // Role returns the role of the report column. 33 | func (*annotationReportColumn) Role() string { 34 | return "annotation" 35 | } 36 | 37 | // Value returns the next data value for the report column. 38 | func (c *annotationReportColumn) Value() string { 39 | value := <-c.values 40 | 41 | if value != "" { 42 | return fmt.Sprintf("%q", value) 43 | } 44 | 45 | return "null" 46 | } 47 | -------------------------------------------------------------------------------- /helper/apply.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Apply applies the given transformation function to each element in the 8 | // input channel and returns a new channel containing the transformed 9 | // values. The transformation function takes a float64 value as input 10 | // and returns a float64 value as output. 11 | // 12 | // Example: 13 | // 14 | // timesTwo := helper.Apply(c, func(n int) int { 15 | // return n * 2 16 | // }) 17 | func Apply[T Number](c <-chan T, f func(T) T) <-chan T { 18 | ac := make(chan T) 19 | 20 | go func() { 21 | defer close(ac) 22 | 23 | for n := range c { 24 | ac <- f(n) 25 | } 26 | }() 27 | 28 | return ac 29 | } 30 | -------------------------------------------------------------------------------- /helper/apply_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestApply(t *testing.T) { 14 | input := helper.SliceToChan([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) 15 | expected := helper.SliceToChan([]int{2, 4, 6, 8, 10, 12, 14, 16, 18, 20}) 16 | 17 | actual := helper.Apply(input, func(n int) int { 18 | return n * 2 19 | }) 20 | 21 | err := helper.CheckEquals(actual, expected) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /helper/buffered.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Buffered takes a channel of any type and returns a new channel of the same type with 8 | // a buffer of the specified size. This allows the original channel to continue sending 9 | // data even if the receiving end is temporarily unavailable. 10 | // 11 | // Example: 12 | func Buffered[T any](c <-chan T, size int) <-chan T { 13 | result := make(chan T, size) 14 | 15 | go Pipe(c, result) 16 | 17 | return result 18 | } 19 | -------------------------------------------------------------------------------- /helper/buffered_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestBuffered(_ *testing.T) { 14 | c := make(chan int, 1) 15 | b := helper.Buffered(c, 4) 16 | 17 | c <- 1 18 | c <- 2 19 | c <- 3 20 | c <- 4 21 | 22 | close(c) 23 | 24 | helper.Drain(b) 25 | } 26 | -------------------------------------------------------------------------------- /helper/chan_to_json.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | import ( 8 | "encoding/json" 9 | "io" 10 | ) 11 | 12 | // ChanToJSON converts a channel of values into JSON format and writes it to the specified writer. 13 | // 14 | // Example: 15 | // 16 | // input := helper.SliceToChan([]int{2, 4, 6, 8}) 17 | // 18 | // var buffer bytes.Buffer 19 | // err := helper.ChanToJSON(input, &buffer) 20 | // 21 | // fmt.Println(buffer.String()) 22 | // // Output: [2,4,6,8,9] 23 | func ChanToJSON[T any](c <-chan T, w io.Writer) error { 24 | first := true 25 | 26 | _, err := w.Write([]byte{'['}) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | for n := range c { 32 | if !first { 33 | _, err = w.Write([]byte{','}) 34 | if err != nil { 35 | return err 36 | } 37 | } else { 38 | first = false 39 | } 40 | 41 | encoded, err := json.Marshal(n) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | _, err = w.Write(encoded) 47 | if err != nil { 48 | return err 49 | } 50 | } 51 | 52 | _, err = w.Write([]byte{']'}) 53 | 54 | return err 55 | } 56 | -------------------------------------------------------------------------------- /helper/chan_to_json_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | 11 | "github.com/cinar/indicator/v2/helper" 12 | ) 13 | 14 | func TestChanToJSON(t *testing.T) { 15 | input := helper.SliceToChan([]int{2, 4, 6, 8}) 16 | expected := "[2,4,6,8]" 17 | 18 | var buffer bytes.Buffer 19 | 20 | err := helper.ChanToJSON(input, &buffer) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | actual := buffer.String() 26 | if actual != expected { 27 | t.Fatalf("actual=%s expected=%s", actual, expected) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /helper/chan_to_slice.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // ChanToSlice converts a channel of float64 to a slice of float64. 8 | // 9 | // Example: 10 | // 11 | // c := make(chan int, 4) 12 | // c <- 1 13 | // c <- 2 14 | // c < -3 15 | // c <- 4 16 | // close(c) 17 | // 18 | // fmt.Println(helper.ChanToSlice(c)) // [1, 2, 3, 4] 19 | func ChanToSlice[T any](c <-chan T) []T { 20 | var slice []T 21 | 22 | for n := range c { 23 | slice = append(slice, n) 24 | } 25 | 26 | return slice 27 | } 28 | -------------------------------------------------------------------------------- /helper/chan_to_slice_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestChanToSlice(t *testing.T) { 14 | input := []int{2, 4, 6, 8} 15 | expected := helper.SliceToChan(input) 16 | 17 | actual := make(chan int, len(input)) 18 | for _, n := range input { 19 | actual <- n 20 | } 21 | close(actual) 22 | 23 | err := helper.CheckEquals(actual, expected) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /helper/change.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Change calculates the difference between the current value and the value N before. 8 | // 9 | // Example: 10 | // 11 | // input := []int{1, 2, 5, 5, 8, 2, 1, 1, 3, 4} 12 | // output := helper.Change(helper.SliceToChan(input), 2) 13 | // fmt.Println(helper.ChanToSlice(output)) // [4, 3, 3, -3, -7, -1, 2, 3] 14 | func Change[T Number](c <-chan T, before int) <-chan T { 15 | cs := Duplicate(c, 2) 16 | cs[0] = Buffered(cs[0], before) 17 | cs[1] = Skip(cs[1], before) 18 | 19 | return Subtract(cs[1], cs[0]) 20 | } 21 | -------------------------------------------------------------------------------- /helper/change_percent.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // ChangePercent calculates the percentage change between the current 8 | // value and the value N positions before. 9 | // 10 | // Example: 11 | // 12 | // c := helper.ChanToSlice([]float64{1, 2, 5, 5, 8, 2, 1, 1, 3, 4}) 13 | // actual := helper.ChangePercent(c, 2)) 14 | // fmt.Println(helper.ChanToSlice(actual)) // [400, 150, 60, -60, -87.5, -50, 200, 300] 15 | func ChangePercent[T Number](c <-chan T, before int) <-chan T { 16 | return MultiplyBy(ChangeRatio(c, before), 100) 17 | } 18 | -------------------------------------------------------------------------------- /helper/change_percent_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestChangePercent(t *testing.T) { 14 | input := helper.SliceToChan([]float64{1, 2, 5, 5, 8, 2, 1, 1, 3, 4}) 15 | expected := helper.SliceToChan([]float64{400, 150, 60, -60, -87.5, -50, 200, 300}) 16 | 17 | actual := helper.ChangePercent(input, 2) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/change_ratio.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // ChangeRatio calculates the ratio change between the current 8 | // value and the value N positions before. 9 | // 10 | // Example: 11 | // 12 | // c := helper.ChanToSlice([]float64{1, 2, 5, 5, 8, 2, 1, 1, 3, 4}) 13 | // actual := helper.ChangeRatio(c, 2)) 14 | // fmt.Println(helper.ChanToSlice(actual)) // [400, 150, 60, -60, -87.5, -50, 200, 300] 15 | func ChangeRatio[T Number](c <-chan T, before int) <-chan T { 16 | cs := Duplicate(c, 2) 17 | cs[1] = Buffered(cs[1], before) 18 | return Divide(Change(cs[0], before), cs[1]) 19 | } 20 | -------------------------------------------------------------------------------- /helper/change_ratio_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestChangeRatio(t *testing.T) { 14 | input := helper.SliceToChan([]float64{1, 2, 5, 5, 8, 2, 1, 1, 3, 4}) 15 | expected := helper.SliceToChan([]float64{4, 1.5, 0.6, -0.6, -0.875, -0.5, 2, 3}) 16 | 17 | actual := helper.ChangeRatio(input, 2) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/change_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestChange(t *testing.T) { 14 | input := helper.SliceToChan([]int{1, 2, 5, 5, 8, 2, 1, 1, 3, 4}) 15 | expected := helper.SliceToChan([]int{4, 3, 3, -3, -7, -1, 2, 3}) 16 | 17 | actual := helper.Change(input, 2) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/check.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "reflect" 11 | ) 12 | 13 | // CheckEquals determines whether the two channels are equal. 14 | func CheckEquals[T comparable](inputs ...<-chan T) error { 15 | if len(inputs)%2 != 0 { 16 | return errors.New("not pairs") 17 | } 18 | 19 | i := 0 20 | 21 | for { 22 | for j := 0; j < len(inputs); j += 2 { 23 | actual, actualOk := <-inputs[j] 24 | expected, expectedOk := <-inputs[j+1] 25 | 26 | if !actualOk || !expectedOk { 27 | if actualOk != expectedOk { 28 | return fmt.Errorf("not ended the same actual %v expected %v", actualOk, expectedOk) 29 | } 30 | 31 | return nil 32 | } 33 | 34 | if !reflect.DeepEqual(actual, expected) { 35 | return fmt.Errorf("index %d pair %d actual %v expected %v", i, j/2, actual, expected) 36 | } 37 | } 38 | 39 | i++ 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /helper/check_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestCheckEqualsNotPairs(t *testing.T) { 14 | c := helper.SliceToChan([]int{1, 2, 3, 4}) 15 | 16 | err := helper.CheckEquals(c) 17 | if err == nil { 18 | t.Fatal("expected error") 19 | } 20 | } 21 | 22 | func TestCheckEqualsNotEndedTheSame(t *testing.T) { 23 | a := helper.SliceToChan([]int{1, 2, 3, 4}) 24 | b := helper.SliceToChan([]int{1, 2}) 25 | 26 | err := helper.CheckEquals(a, b) 27 | if err == nil { 28 | t.Fatal("expected error") 29 | } 30 | } 31 | 32 | func TestCheckEqualsNotEquals(t *testing.T) { 33 | a := helper.SliceToChan([]int{1, 2, 3, 4}) 34 | b := helper.SliceToChan([]int{1, 3, 3, 4}) 35 | 36 | err := helper.CheckEquals(a, b) 37 | if err == nil { 38 | t.Fatal("expected error") 39 | } 40 | } 41 | 42 | func TestCheckEquals(t *testing.T) { 43 | a := helper.SliceToChan([]int{1, 2, 3, 4}) 44 | b := helper.SliceToChan([]int{1, 2, 3, 4}) 45 | 46 | err := helper.CheckEquals(a, b) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /helper/closer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | import ( 8 | "io" 9 | "log/slog" 10 | ) 11 | 12 | // CloseAndLogError attempts to close the closer and logs any error. 13 | func CloseAndLogError(closer io.Closer, message string) { 14 | CloseAndLogErrorWithLogger(closer, message, slog.Default()) 15 | } 16 | 17 | // CloseAndLogErrorWithLogger attempts to close the closer and logs any error to the given logger. 18 | func CloseAndLogErrorWithLogger(closer io.Closer, message string, logger *slog.Logger) { 19 | err := closer.Close() 20 | if err != nil { 21 | logger.Error(message, "error", err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/closer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "os" 9 | "testing" 10 | 11 | "github.com/cinar/indicator/v2/helper" 12 | ) 13 | 14 | func TestCloseAndLogError(t *testing.T) { 15 | file, err := os.CreateTemp(os.TempDir(), "closer") 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | helper.CloseAndLogError(file, "") 21 | helper.CloseAndLogError(file, "") 22 | } 23 | -------------------------------------------------------------------------------- /helper/count.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Count generates a sequence of numbers starting with a specified value, from, and incrementing by one until 8 | // the given other channel continues to produce values. 9 | // 10 | // Example: 11 | // 12 | // other := make(chan int, 4) 13 | // other <- 1 14 | // other <- 1 15 | // other <- 1 16 | // other <- 1 17 | // close(other) 18 | // 19 | // c := Count(0, other) 20 | // 21 | // fmt.Println(<- s) // 1 22 | // fmt.Println(<- s) // 2 23 | // fmt.Println(<- s) // 3 24 | // fmt.Println(<- s) // 4 25 | func Count[T Number, O any](from T, other <-chan O) <-chan T { 26 | c := make(chan T) 27 | 28 | go func() { 29 | defer close(c) 30 | 31 | for i := from; ; i++ { 32 | _, ok := <-other 33 | if !ok { 34 | break 35 | } 36 | 37 | c <- i 38 | } 39 | }() 40 | 41 | return c 42 | } 43 | -------------------------------------------------------------------------------- /helper/count_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestCount(t *testing.T) { 14 | input := helper.SliceToChan([]int{1, 1, 1, 1}) 15 | expected := helper.SliceToChan([]int{1, 2, 3, 4}) 16 | 17 | actual := helper.Count(1, input) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/database.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | import ( 8 | "database/sql" 9 | "fmt" 10 | "log" 11 | ) 12 | 13 | // CloseDatabaseWithError closes the database after an error. 14 | func CloseDatabaseWithError(db *sql.DB, err error) error { 15 | closeErr := db.Close() 16 | if closeErr == nil { 17 | return err 18 | } 19 | 20 | closeErr = fmt.Errorf("unable to close database: %w", closeErr) 21 | 22 | if err != nil { 23 | log.Println(closeErr) 24 | return err 25 | } 26 | 27 | return closeErr 28 | } 29 | 30 | // CloseDatabaseRows closes the database rows. 31 | func CloseDatabaseRows(rows *sql.Rows) { 32 | err := rows.Close() 33 | if err != nil { 34 | log.Println(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /helper/days_between.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | import ( 8 | "math" 9 | "time" 10 | ) 11 | 12 | // DaysBetween calculates the days between the given two times. 13 | func DaysBetween(from, to time.Time) int { 14 | return int(math.Floor(to.Sub(from).Hours() / 24)) 15 | } 16 | -------------------------------------------------------------------------------- /helper/days_between_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | "time" 10 | 11 | "github.com/cinar/indicator/v2/helper" 12 | ) 13 | 14 | func TestDaysBetween(t *testing.T) { 15 | from := time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC) 16 | to := time.Date(2024, 9, 15, 0, 0, 0, 0, time.UTC) 17 | 18 | actual := helper.DaysBetween(from, from) 19 | expected := 0 20 | 21 | if actual != expected { 22 | t.Fatalf("actual %d expected %d", actual, expected) 23 | } 24 | 25 | actual = helper.DaysBetween(from, to) 26 | expected = 14 27 | 28 | if actual != expected { 29 | t.Fatalf("actual %d expected %d", actual, expected) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /helper/decrement_by.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // DecrementBy decrements each element in the input channel by the 8 | // specified decrement value and returns a new channel containing 9 | // the decremented values. 10 | // 11 | // Example: 12 | // 13 | // input := helper.SliceToChan([]int{1, 2, 3, 4}) 14 | // substractOne := helper.DecrementBy(input, 1) 15 | // fmt.Println(helper.ChanToSlice(substractOne)) // [0, 1, 2, 3] 16 | func DecrementBy[T Number](c <-chan T, d T) <-chan T { 17 | return Apply(c, func(n T) T { 18 | return n - d 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /helper/decrement_by_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestDecrementBy(t *testing.T) { 14 | input := []int{2, 3, 4, 5} 15 | expected := helper.SliceToChan([]int{1, 2, 3, 4}) 16 | 17 | actual := helper.DecrementBy(helper.SliceToChan(input), 1) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/divide.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Divide takes two channels of float64 and divides the values 8 | // from the first channel with the values from the second one. 9 | // It returns a new channel containing the results of 10 | // the division. 11 | // 12 | // Example: 13 | // 14 | // ac := helper.SliceToChan([]int{2, 4, 6, 8, 10}) 15 | // bc := helper.SliceToChan([]int{2, 1, 3, 2, 5}) 16 | // 17 | // division := helper.Divide(ac, bc) 18 | // 19 | // fmt.Println(helper.ChanToSlice(division)) // [1, 4, 2, 4, 2] 20 | func Divide[T Number](ac, bc <-chan T) <-chan T { 21 | return Operate(ac, bc, func(a, b T) T { 22 | return a / b 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /helper/divide_by.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // DivideBy divides each element in the input channel 8 | // of float64 values by the given divider and returns a 9 | // new channel containing the divided values. 10 | // 11 | // Example: 12 | // 13 | // half := helper.DivideBy(helper.SliceToChan([]int{2, 4, 6, 8}), 2) 14 | // fmt.Println(helper.ChanToSlice(half)) // [1, 2, 3, 4] 15 | func DivideBy[T Number](c <-chan T, d T) <-chan T { 16 | return Apply(c, func(n T) T { 17 | return n / d 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /helper/divide_by_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestDivideBy(t *testing.T) { 14 | input := []int{2, 4, 6, 8} 15 | expected := helper.SliceToChan([]int{1, 2, 3, 4}) 16 | 17 | actual := helper.DivideBy(helper.SliceToChan(input), 2) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/divide_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestDivide(t *testing.T) { 14 | ac := helper.SliceToChan([]int{2, 4, 6, 8, 10}) 15 | bc := helper.SliceToChan([]int{2, 1, 3, 2, 5}) 16 | 17 | expected := helper.SliceToChan([]int{1, 4, 2, 4, 2}) 18 | 19 | actual := helper.Divide(ac, bc) 20 | 21 | err := helper.CheckEquals(actual, expected) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /helper/drain.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Drain drains the given channel. It blocks the caller. 8 | func Drain[T any](c <-chan T) { 9 | for { 10 | _, ok := <-c 11 | if !ok { 12 | break 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /helper/drain_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestDrain(_ *testing.T) { 14 | input := helper.SliceToChan([]int{2, 4, 6, 8}) 15 | helper.Drain(input) 16 | } 17 | -------------------------------------------------------------------------------- /helper/duplicate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Duplicate duplicates a given receive-only channel by reading each value coming out of 8 | // that channel and sending them on requested number of new output channels. 9 | // 10 | // Example: 11 | // 12 | // expected := helper.SliceToChan([]float64{-10, 20, -4, -5}) 13 | // outputs := helper.Duplicates[float64](helper.SliceToChan(expected), 2) 14 | // 15 | // fmt.Println(helper.ChanToSlice(outputs[0])) // [-10, 20, -4, -5] 16 | // fmt.Println(helper.ChanToSlice(outputs[1])) // [-10, 20, -4, -5] 17 | func Duplicate[T any](input <-chan T, count int) []<-chan T { 18 | // TODO(cinar): Find a way to cast as a directional channel array. 19 | outputs := make([]chan T, count) 20 | result := make([]<-chan T, count) 21 | 22 | for i := range outputs { 23 | outputs[i] = make(chan T, cap(input)) 24 | result[i] = outputs[i] 25 | } 26 | 27 | go func() { 28 | for _, output := range outputs { 29 | defer close(output) 30 | } 31 | 32 | for n := range input { 33 | for _, output := range outputs { 34 | output <- n 35 | } 36 | } 37 | }() 38 | 39 | return result 40 | } 41 | -------------------------------------------------------------------------------- /helper/duplicate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestDuplicate(t *testing.T) { 14 | expecteds := []float64{-10, 20, -4, -5} 15 | 16 | outputs := helper.Duplicate[float64](helper.SliceToChan(expecteds), 4) 17 | 18 | for i, expected := range expecteds { 19 | for _, output := range outputs { 20 | actual := <-output 21 | if actual != expected { 22 | t.Fatalf("index %d actual %v expected %v", i, actual, expected) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /helper/echo.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Echo takes a channel of numbers, repeats the specified count of numbers at the end by the specified count. 8 | // 9 | // Example: 10 | // 11 | // input := helper.SliceToChan([]int{2, 4, 6, 8}) 12 | // output := helper.Echo(input, 2, 4)) 13 | // fmt.Println(helper.ChanToSlice(output)) // [2, 4, 6, 8, 6, 8, 6, 8, 6, 8, 6, 8] 14 | func Echo[T any](input <-chan T, last, count int) <-chan T { 15 | output := make(chan T) 16 | memory := NewRing[T](last) 17 | 18 | go func() { 19 | defer close(output) 20 | 21 | for n := range input { 22 | memory.Put(n) 23 | output <- n 24 | } 25 | 26 | for i := 0; i < count; i++ { 27 | for j := 0; j < last; j++ { 28 | output <- memory.At(j) 29 | } 30 | } 31 | }() 32 | 33 | return output 34 | } 35 | -------------------------------------------------------------------------------- /helper/echo_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestEcho(t *testing.T) { 14 | input := helper.SliceToChan([]int{2, 4, 6, 8}) 15 | expected := helper.SliceToChan([]int{2, 4, 6, 8, 6, 8, 6, 8, 6, 8, 6, 8}) 16 | 17 | actual := helper.Echo(input, 2, 4) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/field.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | import ( 8 | "errors" 9 | "reflect" 10 | ) 11 | 12 | // Field extracts a specific field from a channel of struct pointers and 13 | // delivers it through a new channel. 14 | func Field[T, S any](c <-chan *S, name string) (<-chan T, error) { 15 | st := reflect.TypeOf((*S)(nil)).Elem() 16 | if st.Kind() != reflect.Struct { 17 | return nil, errors.New("type not a struct") 18 | } 19 | 20 | f, ok := st.FieldByName(name) 21 | if !ok { 22 | return nil, errors.New("field is not found") 23 | } 24 | 25 | result := make(chan T, cap(c)) 26 | 27 | go func() { 28 | defer close(result) 29 | 30 | for n := range c { 31 | v := reflect.ValueOf(n).Elem() 32 | result <- v.FieldByIndex(f.Index).Interface().(T) 33 | } 34 | }() 35 | 36 | return result, nil 37 | } 38 | -------------------------------------------------------------------------------- /helper/field_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestField(t *testing.T) { 14 | type Row struct { 15 | Open float64 16 | Close float64 17 | } 18 | 19 | row := &Row{ 20 | Open: 1, 21 | Close: 2, 22 | } 23 | 24 | rows := make(chan *Row, 1) 25 | rows <- row 26 | close(rows) 27 | 28 | closeField, err := helper.Field[float64](rows, "Close") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | closeValue := <-closeField 34 | 35 | if closeValue != row.Close { 36 | t.Fatalf("actual %v expected %v", closeValue, row.Close) 37 | } 38 | } 39 | 40 | func TestFieldNotStruct(t *testing.T) { 41 | c := make(chan *float64) 42 | close(c) 43 | 44 | _, err := helper.Field[float64](c, "Name") 45 | if err == nil { 46 | t.Fatal("expecting error") 47 | } 48 | } 49 | 50 | func TestFieldUnknownName(t *testing.T) { 51 | type Row struct { 52 | Open float64 53 | Close float64 54 | } 55 | 56 | c := make(chan *Row) 57 | close(c) 58 | 59 | _, err := helper.Field[float64](c, "High") 60 | if err == nil { 61 | t.Fatal("expecting error") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /helper/filter.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Filter filters the items from the input channel based on the 8 | // provided predicate function. The predicate function takes a 9 | // float64 value as input and returns a boolean value indicating 10 | // whether the value should be included in the output channel. 11 | // 12 | // Example: 13 | // 14 | // even := helper.Filter(c, func(n int) bool { 15 | // return n%2 == 0 16 | // }) 17 | func Filter[T any](c <-chan T, p func(T) bool) <-chan T { 18 | fc := make(chan T) 19 | 20 | go func() { 21 | for n := range c { 22 | if p(n) { 23 | fc <- n 24 | } 25 | } 26 | 27 | close(fc) 28 | }() 29 | 30 | return fc 31 | } 32 | -------------------------------------------------------------------------------- /helper/filter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestFilter(t *testing.T) { 14 | input := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 15 | expected := helper.SliceToChan([]int{2, 4, 6, 8, 10}) 16 | 17 | actual := helper.Filter(helper.SliceToChan(input), func(n int) bool { 18 | return n%2 == 0 19 | }) 20 | 21 | err := helper.CheckEquals(actual, expected) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /helper/first.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // First takes a channel of values and returns a new channel containing the first N values. 8 | func First[T any](c <-chan T, count int) <-chan T { 9 | result := make(chan T, cap(c)) 10 | 11 | go func() { 12 | for i := 0; i < count; i++ { 13 | n, ok := <-c 14 | if !ok { 15 | break 16 | } 17 | 18 | result <- n 19 | } 20 | 21 | close(result) 22 | 23 | Drain(c) 24 | }() 25 | 26 | return result 27 | } 28 | -------------------------------------------------------------------------------- /helper/first_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestFirst(t *testing.T) { 14 | input := helper.SliceToChan([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) 15 | expected := helper.SliceToChan([]int{1, 2, 3, 4}) 16 | 17 | actual := helper.First(input, 4) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | 25 | func TestFirstLessValues(t *testing.T) { 26 | input := helper.SliceToChan([]int{1, 2}) 27 | expected := helper.SliceToChan([]int{1, 2}) 28 | 29 | actual := helper.First(input, 4) 30 | 31 | err := helper.CheckEquals(actual, expected) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /helper/gcd.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Gcd calculates the Greatest Common Divisor of the given numbers. 8 | func Gcd(values ...int) int { 9 | gcd := values[0] 10 | 11 | for i := 1; i < len(values); i++ { 12 | value := values[i] 13 | 14 | for value > 0 { 15 | gcd, value = value, gcd%value 16 | } 17 | 18 | if gcd == 1 { 19 | break 20 | } 21 | } 22 | 23 | return gcd 24 | } 25 | -------------------------------------------------------------------------------- /helper/gcd_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestGcdWithTwoValues(t *testing.T) { 14 | actual := helper.Gcd(1220, 512) 15 | expected := 4 16 | 17 | if actual != expected { 18 | t.Fatalf("actual %d expected %d", actual, expected) 19 | } 20 | } 21 | 22 | func TestGcdWithThreeValues(t *testing.T) { 23 | actual := helper.Gcd(1, 2, 5) 24 | expected := 1 25 | 26 | if actual != expected { 27 | t.Fatalf("actual %d expected %d", actual, expected) 28 | } 29 | } 30 | 31 | func TestGcdWithFourValues(t *testing.T) { 32 | actual := helper.Gcd(2, 4, 6, 12) 33 | expected := 2 34 | 35 | if actual != expected { 36 | t.Fatalf("actual %d expected %d", actual, expected) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /helper/head.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Head retrieves the specified number of elements 8 | // from the given channel of float64 values and 9 | // delivers them through a new channel. 10 | // 11 | // Example: 12 | // 13 | // c := helper.SliceToChan([]int{2, 4, 6, 8}) 14 | // actual := helper.Head(c, 2) 15 | // fmt.Println(helper.ChanToSlice(actual)) // [2, 4] 16 | func Head[T Number](c <-chan T, count int) <-chan T { 17 | result := make(chan T, cap(c)) 18 | 19 | go func() { 20 | defer close(result) 21 | 22 | for i := 0; i < count; i++ { 23 | n, ok := <-c 24 | if !ok { 25 | break 26 | } 27 | 28 | result <- n 29 | } 30 | }() 31 | 32 | return result 33 | } 34 | -------------------------------------------------------------------------------- /helper/head_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestHead(t *testing.T) { 14 | input := []int{2, 4, 6, 8} 15 | expected := helper.SliceToChan([]int{2, 4}) 16 | 17 | actual := helper.Head(helper.SliceToChan(input), 2) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | 25 | func TestHeadEarly(t *testing.T) { 26 | input := []int{2} 27 | expected := helper.SliceToChan([]int{2}) 28 | 29 | actual := helper.Head(helper.SliceToChan(input), 2) 30 | 31 | err := helper.CheckEquals(actual, expected) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /helper/helper.go: -------------------------------------------------------------------------------- 1 | // Package helper contains the helper functions. 2 | // 3 | // This package belongs to the Indicator project. Indicator is 4 | // a Golang module that supplies a variety of technical 5 | // indicators, strategies, and a backtesting framework 6 | // for analysis. 7 | // 8 | // # License 9 | // 10 | // Copyright (c) 2021-2024 Onur Cinar. 11 | // The source code is provided under GNU AGPLv3 License. 12 | // https://github.com/cinar/indicator 13 | // 14 | // # Disclaimer 15 | // 16 | // The information provided on this project is strictly for 17 | // informational purposes and is not to be construed as 18 | // advice or solicitation to buy or sell any security. 19 | package helper 20 | 21 | // Integer refers to any integer type. 22 | type Integer interface { 23 | int | int8 | int16 | int32 | int64 24 | } 25 | 26 | // Float refers to any float type. 27 | type Float interface { 28 | float32 | float64 29 | } 30 | 31 | // Number refers to any numeric type. 32 | type Number interface { 33 | Integer | Float 34 | } 35 | -------------------------------------------------------------------------------- /helper/increment_by.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // IncrementBy increments each element in the input channel by the 8 | // specified increment value and returns a new channel containing 9 | // the incremented values. 10 | // 11 | // Example: 12 | // 13 | // input := []int{1, 2, 3, 4} 14 | // actual := helper.IncrementBy(helper.SliceToChan(input), 1) 15 | // fmt.Println(helper.ChanToSlice(actual)) // [2, 3, 4, 5] 16 | func IncrementBy[T Number](c <-chan T, i T) <-chan T { 17 | return Apply(c, func(n T) T { 18 | return n + i 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /helper/increment_by_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestIncrementBy(t *testing.T) { 14 | input := []int{1, 2, 3, 4} 15 | expected := helper.SliceToChan([]int{2, 3, 4, 5}) 16 | 17 | actual := helper.IncrementBy(helper.SliceToChan(input), 1) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/json_to_chan_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "strings" 9 | "testing" 10 | 11 | "github.com/cinar/indicator/v2/helper" 12 | ) 13 | 14 | func TestJSONToChan(t *testing.T) { 15 | expected := helper.SliceToChan([]int{2, 4, 6, 8}) 16 | input := "[2, 4, 6, 8]" 17 | 18 | actual := helper.JSONToChan[int](strings.NewReader(input)) 19 | 20 | err := helper.CheckEquals(actual, expected) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /helper/keep_negatives.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // KeepNegatives processes a stream of float64 values, retaining negative 8 | // values unchanged and replacing positive values with zero. 9 | // 10 | // Example: 11 | // 12 | // c := helper.SliceToChan([]int{-10, 20, 4, -5}) 13 | // negatives := helper.KeepPositives(c) 14 | // fmt.Println(helper.ChanToSlice(negatives)) // [-10, 0, 0, -5] 15 | func KeepNegatives[T Number](c <-chan T) <-chan T { 16 | return Apply(c, func(n T) T { 17 | if n < 0 { 18 | return n 19 | } 20 | 21 | return 0 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /helper/keep_negatives_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestKeepNegatives(t *testing.T) { 14 | input := []int{-10, 20, 4, -5} 15 | expected := helper.SliceToChan([]int{-10, 0, 0, -5}) 16 | 17 | actual := helper.KeepNegatives(helper.SliceToChan(input)) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/keep_positives.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // KeepPositives processes a stream of float64 values, retaining positive 8 | // values unchanged and replacing negative values with zero. 9 | // 10 | // Example: 11 | // 12 | // c := helper.SliceToChan([]int{-10, 20, 4, -5}) 13 | // positives := helper.KeepPositives(c) 14 | // fmt.Println(helper.ChanToSlice(positives)) // [0, 20, 4, 0] 15 | func KeepPositives[T Number](c <-chan T) <-chan T { 16 | return Apply(c, func(n T) T { 17 | if n > 0 { 18 | return n 19 | } 20 | 21 | return 0 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /helper/keep_positives_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestKeepPositives(t *testing.T) { 14 | input := []int{-10, 20, 4, -5} 15 | expected := helper.SliceToChan([]int{0, 20, 4, 0}) 16 | 17 | actual := helper.KeepPositives(helper.SliceToChan(input)) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/last.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Last takes a channel of values and returns a new channel containing the last N values. 8 | func Last[T any](c <-chan T, count int) <-chan T { 9 | result := make(chan T, cap(c)) 10 | 11 | go func() { 12 | defer close(result) 13 | 14 | ring := NewRing[T](count) 15 | 16 | for n := range c { 17 | ring.Put(n) 18 | } 19 | 20 | for !ring.IsEmpty() { 21 | n, _ := ring.Get() 22 | result <- n 23 | } 24 | }() 25 | 26 | return result 27 | } 28 | -------------------------------------------------------------------------------- /helper/last_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestLast(t *testing.T) { 14 | input := helper.SliceToChan([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) 15 | expected := helper.SliceToChan([]int{7, 8, 9, 10}) 16 | 17 | actual := helper.Last(input, 4) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | 25 | func TestLastLessValues(t *testing.T) { 26 | input := helper.SliceToChan([]int{1, 2}) 27 | expected := helper.SliceToChan([]int{1, 2}) 28 | 29 | actual := helper.Last(input, 4) 30 | 31 | err := helper.CheckEquals(actual, expected) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /helper/lcm.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Lcm calculates the Least Common Multiple of the given numbers. 8 | func Lcm(values ...int) int { 9 | lcm := values[0] 10 | 11 | for i := 1; i < len(values); i++ { 12 | lcm = (values[i] * lcm) / Gcd(values[i], lcm) 13 | } 14 | 15 | return lcm 16 | } 17 | -------------------------------------------------------------------------------- /helper/lcm_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestLcmWithTwoValues(t *testing.T) { 14 | actual := helper.Lcm(18, 32) 15 | expected := 288 16 | 17 | if actual != expected { 18 | t.Fatalf("actual %d expected %d", actual, expected) 19 | } 20 | } 21 | 22 | func TestLcmWithFourValues(t *testing.T) { 23 | actual := helper.Lcm(1, 2, 8, 6) 24 | expected := 24 25 | 26 | if actual != expected { 27 | t.Fatalf("actual %d expected %d", actual, expected) 28 | } 29 | } 30 | 31 | func TestLcmWithFiveValues(t *testing.T) { 32 | actual := helper.Lcm(2, 7, 3, 9, 8) 33 | expected := 504 34 | 35 | if actual != expected { 36 | t.Fatalf("actual %d expected %d", actual, expected) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /helper/map.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Map applies the given transformation function to each element in the 8 | // input channel and returns a new channel containing the transformed 9 | // values. The transformation function takes a float64 value as input 10 | // and returns a float64 value as output. 11 | // 12 | // Example: 13 | // 14 | // timesTwo := helper.Map(c, func(n int) int { 15 | // return n * 2 16 | // }) 17 | func Map[F, T any](c <-chan F, f func(F) T) <-chan T { 18 | mc := make(chan T) 19 | 20 | go func() { 21 | defer close(mc) 22 | 23 | for n := range c { 24 | mc <- f(n) 25 | } 26 | }() 27 | 28 | return mc 29 | } 30 | -------------------------------------------------------------------------------- /helper/map_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestMap(t *testing.T) { 14 | type Row struct { 15 | High int 16 | Low int 17 | } 18 | 19 | input := []Row{ 20 | {High: 10, Low: 5}, 21 | {High: 20, Low: 15}, 22 | } 23 | 24 | expected := helper.SliceToChan([]int{5, 15}) 25 | 26 | actual := helper.Map(helper.SliceToChan(input), func(r Row) int { 27 | return r.Low 28 | }) 29 | 30 | err := helper.CheckEquals(actual, expected) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /helper/map_with_previous.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // MapWithPrevious applies a transformation function to each element in an input channel, creating a new channel 8 | // with the transformed values. It maintains a "memory" of the previous result, allowing the transformation 9 | // function to consider both the current element and the outcome of the previous transformation. This 10 | // enables functions that rely on accumulated state or sequential dependencies between elements. 11 | // 12 | // Example: 13 | // 14 | // sum := helper.MapWithPrevious(c, func(p, c int) int { 15 | // return p + c 16 | // }, 0) 17 | func MapWithPrevious[F, T any](c <-chan F, f func(T, F) T, previous T) <-chan T { 18 | mc := make(chan T) 19 | 20 | go func() { 21 | defer close(mc) 22 | 23 | for n := range c { 24 | previous = f(previous, n) 25 | mc <- previous 26 | } 27 | }() 28 | 29 | return mc 30 | } 31 | -------------------------------------------------------------------------------- /helper/map_with_previous_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestMapWithPrevious(t *testing.T) { 14 | input := helper.SliceToChan([]int{1, 2, 3, 4}) 15 | expected := helper.SliceToChan([]int{1, 3, 6, 10}) 16 | 17 | actual := helper.MapWithPrevious(input, func(p, c int) int { 18 | return p + c 19 | }, 0) 20 | 21 | err := helper.CheckEquals(actual, expected) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /helper/multiply.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Multiply takes two channels of float64 and multiples the values 8 | // from the first channel with the values from the second channel. 9 | // It returns a new channel containing the results of 10 | // the multiplication. 11 | // 12 | // Example: 13 | // 14 | // ac := helper.SliceToChan([]int{1, 4, 2, 4, 2}) 15 | // bc := helper.SliceToChan([]int{2, 1, 3, 2, 5}) 16 | // 17 | // multiplication := helper.Multiply(ac, bc) 18 | // 19 | // fmt.Println(helper.ChanToSlice(multiplication)) // [2, 4, 6, 8, 10] 20 | func Multiply[T Number](ac, bc <-chan T) <-chan T { 21 | return Operate(ac, bc, func(a, b T) T { 22 | return a * b 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /helper/multiply_by.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // MultiplyBy multiplies each element in the input channel 8 | // of float64 values by the given multiplier and returns a 9 | // new channel containing the multiplied values. 10 | // 11 | // Example: 12 | // 13 | // c := helper.SliceToChan([]int{1, 2, 3, 4}) 14 | // twoTimes := helper.MultiplyBy(c, 2) 15 | // fmt.Println(helper.ChanToSlice(twoTimes)) // [2, 4, 6, 8] 16 | func MultiplyBy[T Number](c <-chan T, m T) <-chan T { 17 | return Apply(c, func(n T) T { 18 | return n * m 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /helper/multiply_by_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestMultiplyBy(t *testing.T) { 14 | input := []int{1, 2, 3, 4} 15 | expected := helper.SliceToChan([]int{2, 4, 6, 8}) 16 | 17 | actual := helper.MultiplyBy(helper.SliceToChan(input), 2) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/multiply_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestMultiply(t *testing.T) { 14 | ac := helper.SliceToChan([]int{1, 4, 2, 4, 2}) 15 | bc := helper.SliceToChan([]int{2, 1, 3, 2, 5}) 16 | 17 | expected := helper.SliceToChan([]int{2, 4, 6, 8, 10}) 18 | 19 | actual := helper.Multiply(ac, bc) 20 | 21 | err := helper.CheckEquals(actual, expected) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /helper/numeric_report_column.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | import "fmt" 8 | 9 | // numericReportColumn is the number report column struct. 10 | type numericReportColumn[T Number] struct { 11 | ReportColumn 12 | name string 13 | values <-chan T 14 | } 15 | 16 | // NewNumericReportColumn returns a new instance of a numeric data column for a report. 17 | func NewNumericReportColumn[T Number](name string, values <-chan T) ReportColumn { 18 | return &numericReportColumn[T]{ 19 | name: name, 20 | values: values, 21 | } 22 | } 23 | 24 | // Name returns the name of the report column. 25 | func (c *numericReportColumn[T]) Name() string { 26 | return c.name 27 | } 28 | 29 | // Type returns number as the data type. 30 | func (*numericReportColumn[T]) Type() string { 31 | return "number" 32 | } 33 | 34 | // Role returns the role of the report column. 35 | func (*numericReportColumn[T]) Role() string { 36 | return "data" 37 | } 38 | 39 | // Value returns the next data value for the report column. 40 | func (c *numericReportColumn[T]) Value() string { 41 | return fmt.Sprintf("%v", <-c.values) 42 | } 43 | -------------------------------------------------------------------------------- /helper/operate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Operate applies the provided operate function to corresponding values from two 8 | // numeric input channels and sends the resulting values to an output channel. 9 | // 10 | // Example: 11 | // 12 | // add := helper.Operate(ac, bc, func(a, b int) int { 13 | // return a + b 14 | // }) 15 | func Operate[A any, B any, R any](ac <-chan A, bc <-chan B, o func(A, B) R) <-chan R { 16 | oc := make(chan R) 17 | 18 | go func() { 19 | defer close(oc) 20 | 21 | for { 22 | an, ok := <-ac 23 | if !ok { 24 | Drain(bc) 25 | break 26 | } 27 | 28 | bn, ok := <-bc 29 | if !ok { 30 | Drain(ac) 31 | break 32 | } 33 | 34 | oc <- o(an, bn) 35 | } 36 | }() 37 | 38 | return oc 39 | } 40 | -------------------------------------------------------------------------------- /helper/operate3.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Operate3 applies the provided operate function to corresponding values from three 8 | // numeric input channels and sends the resulting values to an output channel. 9 | // 10 | // Example: 11 | // 12 | // add := helper.Operate3(ac, bc, cc, func(a, b, c int) int { 13 | // return a + b + c 14 | // }) 15 | func Operate3[A any, B any, C any, R any](ac <-chan A, bc <-chan B, cc <-chan C, o func(A, B, C) R) <-chan R { 16 | rc := make(chan R) 17 | 18 | go func() { 19 | defer close(rc) 20 | 21 | for { 22 | an, ok := <-ac 23 | if !ok { 24 | break 25 | } 26 | 27 | bn, ok := <-bc 28 | if !ok { 29 | break 30 | } 31 | 32 | cn, ok := <-cc 33 | if !ok { 34 | break 35 | } 36 | 37 | rc <- o(an, bn, cn) 38 | } 39 | 40 | Drain(ac) 41 | Drain(bc) 42 | Drain(cc) 43 | }() 44 | 45 | return rc 46 | } 47 | -------------------------------------------------------------------------------- /helper/pipe.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Pipe function takes an input channel and an output channel and copies 8 | // all elements from the input channel into the output channel. 9 | // 10 | // Example: 11 | // 12 | // input := helper.SliceToChan([]int{2, 4, 6, 8}) 13 | // output := make(chan int) 14 | // helper.Pipe(input, output) 15 | // fmt.println(helper.ChanToSlice(output)) // [2, 4, 6, 8] 16 | func Pipe[T any](f <-chan T, t chan<- T) { 17 | defer close(t) 18 | for n := range f { 19 | t <- n 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /helper/pipe_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestPipe(t *testing.T) { 14 | data := []int{2, 4, 6, 8} 15 | expected := helper.SliceToChan(data) 16 | 17 | input := helper.SliceToChan(data) 18 | actual := make(chan int) 19 | 20 | go helper.Pipe(input, actual) 21 | 22 | err := helper.CheckEquals(actual, expected) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /helper/pow.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | import "math" 8 | 9 | // Pow takes a channel of float64 values and returns the element-wise 10 | // base-value exponential of y. 11 | // 12 | // Example: 13 | // 14 | // c := helper.SliceToChan([]int{2, 3, 5, 10}) 15 | // squared := helper.Pow(c, 2) 16 | // fmt.Println(helper.ChanToSlice(squared)) // [4, 9, 25, 100] 17 | func Pow[T Number](c <-chan T, y T) <-chan T { 18 | return Apply(c, func(n T) T { 19 | return T(math.Pow(float64(n), float64(y))) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /helper/pow_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestPow(t *testing.T) { 14 | input := helper.SliceToChan([]int{2, 3, 5, 10}) 15 | expected := helper.SliceToChan([]int{4, 9, 25, 100}) 16 | 17 | actual := helper.Pow(input, 2) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/remove.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | import ( 8 | "os" 9 | "testing" 10 | ) 11 | 12 | // Remove removes the file with the given name. 13 | func Remove(t *testing.T, name string) { 14 | t.Helper() 15 | 16 | err := os.Remove(name) 17 | if err != nil { 18 | t.Errorf("Error removing file: %v", err) 19 | } 20 | } 21 | 22 | // RemoveAll removes the files with the given path. 23 | func RemoveAll(t *testing.T, path string) { 24 | t.Helper() 25 | 26 | err := os.RemoveAll(path) 27 | if err != nil { 28 | t.Errorf("Error removing files: %v", err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /helper/ring_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestRing(t *testing.T) { 14 | input := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 15 | expected := []int{0, 0, 0, 0, 1, 2, 3, 4, 5, 6} 16 | 17 | ring := helper.NewRing[int](4) 18 | 19 | for i, n := range input { 20 | actual := ring.Put(n) 21 | if actual != expected[i] { 22 | t.Fatalf("actual %v expected %v", actual, expected[i]) 23 | } 24 | } 25 | } 26 | 27 | func TestRingEmpty(t *testing.T) { 28 | input := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 29 | size := 4 30 | 31 | ring := helper.NewRing[int](size) 32 | 33 | if !ring.IsEmpty() { 34 | t.Fatal("not empty") 35 | } 36 | 37 | for i, n := range input { 38 | ring.Put(n) 39 | 40 | if ring.IsEmpty() { 41 | t.Fatal("is empty") 42 | } 43 | 44 | j := i 45 | if j >= size { 46 | j = size - 1 47 | } 48 | 49 | if ring.At(j) != n { 50 | t.Fatalf("actual %v expected %v", ring.At(j), n) 51 | } 52 | } 53 | 54 | if !ring.IsFull() { 55 | t.Fatal("not full") 56 | } 57 | 58 | for i := 0; i < size; i++ { 59 | _, ok := ring.Get() 60 | if !ok { 61 | t.Fatal("is empty") 62 | } 63 | } 64 | 65 | if !ring.IsEmpty() { 66 | t.Fatal("not empty") 67 | } 68 | 69 | _, ok := ring.Get() 70 | if ok { 71 | t.Fatal("not empty") 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /helper/round_digit.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | import "math" 8 | 9 | // RoundDigit rounds the given float64 number to d decimal places. 10 | // 11 | // Example: 12 | // 13 | // n := helper.RoundDigit(10.1234, 2) 14 | // fmt.Println(n) // 10.12 15 | func RoundDigit[T Number](n T, d int) T { 16 | m := math.Pow(10, float64(d)) 17 | return T(math.Round(float64(n)*m) / m) 18 | } 19 | -------------------------------------------------------------------------------- /helper/round_digit_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestRoundDigit(t *testing.T) { 14 | input := 10.1234 15 | expected := 10.12 16 | 17 | actual := helper.RoundDigit(input, 2) 18 | 19 | if actual != expected { 20 | t.Fatalf("actual %v expected %v", actual, expected) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /helper/round_digits.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // RoundDigits takes a channel of float64 numbers and rounds them to d 8 | // decimal places. 9 | // 10 | // Example: 11 | // 12 | // c := helper.SliceToChan([]float64{10.1234, 5.678, 6.78, 8.91011}) 13 | // rounded := helper.RoundDigits(c, 2) 14 | // fmt.Println(helper.ChanToSlice(rounded)) // [10.12, 5.68, 6.78, 8.91] 15 | func RoundDigits[T Number](c <-chan T, d int) <-chan T { 16 | return Apply(c, func(n T) T { 17 | return RoundDigit(n, d) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /helper/round_digits_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestRoundDigits(t *testing.T) { 14 | input := helper.SliceToChan([]float64{10.1234, 5.678, 6.78, 8.91011}) 15 | expected := helper.SliceToChan([]float64{10.12, 5.68, 6.78, 8.91}) 16 | 17 | actual := helper.RoundDigits(input, 2) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/seq.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Seq generates a sequence of numbers starting with a specified value, 8 | // from, and incrementing by a specified amount, increment, until a 9 | // specified value, to, is reached or exceeded. The sequence includes 10 | // both from and to. 11 | // 12 | // Example: 13 | // 14 | // s := Seq(1, 5, 1) 15 | // defer close(s) 16 | // 17 | // fmt.Println(<- s) // 1 18 | // fmt.Println(<- s) // 2 19 | // fmt.Println(<- s) // 3 20 | // fmt.Println(<- s) // 4 21 | func Seq[T Number](from, to, increment T) <-chan T { 22 | c := make(chan T) 23 | 24 | go func() { 25 | for i := from; i < to; i += increment { 26 | c <- i 27 | } 28 | 29 | close(c) 30 | }() 31 | 32 | return c 33 | } 34 | -------------------------------------------------------------------------------- /helper/seq_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestSeq(t *testing.T) { 14 | expected := helper.SliceToChan([]int{2, 3, 4, 5}) 15 | actual := helper.Seq(2, 6, 1) 16 | 17 | err := helper.CheckEquals(actual, expected) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /helper/shift.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Shift takes a channel of numbers, shifts them to the right by the specified count, 8 | // and fills in any missing values with the provided fill value. 9 | // 10 | // Example: 11 | // 12 | // input := helper.SliceToChan([]int{2, 4, 6, 8}) 13 | // output := helper.Shift(input, 4, 0) 14 | // fmt.Println(helper.ChanToSlice(output)) // [0, 0, 0, 0, 2, 4, 6, 8] 15 | func Shift[T any](c <-chan T, count int, fill T) <-chan T { 16 | result := make(chan T, cap(c)+count) 17 | 18 | go func() { 19 | for i := 0; i < count; i++ { 20 | result <- fill 21 | } 22 | 23 | Pipe(c, result) 24 | }() 25 | 26 | return result 27 | } 28 | -------------------------------------------------------------------------------- /helper/shift_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestShift(t *testing.T) { 14 | input := helper.SliceToChan([]int{2, 4, 6, 8}) 15 | expected := helper.SliceToChan([]int{0, 0, 0, 0, 2, 4, 6, 8}) 16 | 17 | actual := helper.Shift(input, 4, 0) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/sign.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Sign takes a channel of float64 values and returns their signs 8 | // as -1 for negative, 0 for zero, and 1 for positive. 9 | // 10 | // Example: 11 | // 12 | // c := helper.SliceToChan([]int{-10, 20, -4, 0}) 13 | // sign := helper.Sign(c) 14 | // fmt.Println(helper.ChanToSlice(sign)) // [-1, 1, -1, 0] 15 | func Sign[T Number](c <-chan T) <-chan T { 16 | return Apply(c, func(n T) T { 17 | if n > 0 { 18 | return 1 19 | } else if n < 0 { 20 | return -1 21 | } 22 | 23 | return 0 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /helper/sign_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestSign(t *testing.T) { 14 | input := helper.SliceToChan([]int{-10, 20, -4, 0}) 15 | expected := helper.SliceToChan([]int{-1, 1, -1, 0}) 16 | 17 | actual := helper.Sign(input) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/since.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Since counts the number of periods since the last change of 8 | // value in a channel of numbers. 9 | func Since[T comparable, R Number](c <-chan T) <-chan R { 10 | first := true 11 | 12 | var last T 13 | var count R 14 | 15 | return Map(c, func(n T) R { 16 | if first || last != n { 17 | first = false 18 | last = n 19 | count = 0 20 | } else { 21 | count++ 22 | } 23 | 24 | return count 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /helper/since_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestSince(t *testing.T) { 14 | input := helper.SliceToChan([]int{1, 1, 2, 2, 2, 1, 2, 3, 3, 4}) 15 | expected := helper.SliceToChan([]int{0, 1, 0, 1, 2, 0, 0, 0, 1, 0}) 16 | 17 | actual := helper.Since[int, int](input) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/skip.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Skip skips the specified number of elements from the 8 | // given channel of float64. 9 | // 10 | // Example: 11 | // 12 | // c := helper.SliceToChan([]int{2, 4, 6, 8}) 13 | // actual := helper.Skip(c, 2) 14 | // fmt.Println(helper.ChanToSlice(actual)) // [6, 8] 15 | func Skip[T any](c <-chan T, count int) <-chan T { 16 | result := make(chan T, cap(c)) 17 | 18 | go func() { 19 | for i := 0; i < count; i++ { 20 | _, ok := <-c 21 | if !ok { 22 | break 23 | } 24 | } 25 | 26 | Pipe(c, result) 27 | }() 28 | 29 | return result 30 | } 31 | -------------------------------------------------------------------------------- /helper/skip_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestSkip(t *testing.T) { 14 | input := helper.SliceToChan([]int{2, 4, 6, 8}) 15 | expected := helper.SliceToChan([]int{6, 8}) 16 | 17 | actual := helper.Skip(input, 2) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/slice_to_chan.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // SliceToChan converts a slice of float64 to a channel of float64. 8 | // 9 | // Example: 10 | // 11 | // slice := []float64{2, 4, 6, 8} 12 | // c := helper.SliceToChan(slice) 13 | // fmt.Println(<- c) // 2 14 | // fmt.Println(<- c) // 4 15 | // fmt.Println(<- c) // 6 16 | // fmt.Println(<- c) // 8 17 | func SliceToChan[T any](slice []T) <-chan T { 18 | c := make(chan T) 19 | 20 | go func() { 21 | defer close(c) 22 | 23 | for _, n := range slice { 24 | c <- n 25 | } 26 | }() 27 | 28 | return c 29 | } 30 | -------------------------------------------------------------------------------- /helper/slice_to_chan_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/cinar/indicator/v2/helper" 12 | ) 13 | 14 | func TestSliceToChan(t *testing.T) { 15 | expected := []int{2, 4, 6, 8} 16 | actual := helper.ChanToSlice(helper.SliceToChan(expected)) 17 | 18 | if !reflect.DeepEqual(actual, expected) { 19 | t.Fatalf("actual %v expected %v", actual, expected) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /helper/sqrt.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | import "math" 8 | 9 | // Sqrt calculates the square root of each value in a channel of float64. 10 | // 11 | // Example: 12 | // 13 | // c := helper.SliceToChan([]int{9, 81, 16, 100}) 14 | // sqrt := helper.Sqrt(c) 15 | // fmt.Println(helper.ChanToSlice(sqrt)) // [3, 9, 4, 10] 16 | func Sqrt[T Number](c <-chan T) <-chan T { 17 | return Apply(c, func(n T) T { 18 | return T(math.Sqrt(float64(n))) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /helper/sqrt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestSqrt(t *testing.T) { 14 | input := helper.SliceToChan([]int{9, 81, 16, 100}) 15 | expected := helper.SliceToChan([]int{3, 9, 4, 10}) 16 | 17 | actual := helper.Sqrt(input) 18 | 19 | err := helper.CheckEquals(actual, expected) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helper/subtract.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | // Subtract takes two channels of float64 and subtracts the values 8 | // from the second channel from the first one. It returns a new 9 | // channel containing the results of the subtractions. 10 | // 11 | // Example: 12 | // 13 | // ac := helper.SliceToChan([]int{2, 4, 6, 8, 10}) 14 | // bc := helper.SliceToChan([]int{1, 2, 3, 4, 5}) 15 | // actual := helper.Subtract(ac, bc) 16 | // fmt.Println(helper.ChanToSlice(actual)) // [1, 2, 3, 4, 5] 17 | func Subtract[T Number](ac, bc <-chan T) <-chan T { 18 | return Operate(ac, bc, func(a, b T) T { 19 | return a - b 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /helper/subtract_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestSubstract(t *testing.T) { 14 | ac := helper.SliceToChan([]int{2, 4, 6, 8, 10}) 15 | bc := helper.SliceToChan([]int{1, 2, 3, 4, 5}) 16 | 17 | expected := helper.SliceToChan([]int{1, 2, 3, 4, 5}) 18 | 19 | actual := helper.Subtract(ac, bc) 20 | 21 | err := helper.CheckEquals(actual, expected) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /helper/sync.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | import "slices" 8 | 9 | // CommonPeriod calculates the smallest period at which all data channels can be synchronized 10 | // 11 | // Example: 12 | // 13 | // // Synchronize channels with periods 4, 2, and 3. 14 | // commonPeriod := helper.CommonPeriod(4, 2, 3) // commonPeriod = 4 15 | // 16 | // // Synchronize the first channel 17 | // c1 := helper.Sync(commonPeriod, 4, c1) 18 | // 19 | // // Synchronize the second channel 20 | // c2 := helper.Sync(commonPeriod, 2, c2) 21 | // 22 | // // Synchronize the third channel 23 | // c3 := helper.Sync(commonPeriod, 3, c3) 24 | func CommonPeriod(periods ...int) int { 25 | return slices.Max(periods) 26 | } 27 | 28 | // SyncPeriod adjusts the given channel to match the given common period. 29 | func SyncPeriod[T any](commonPeriod, period int, c <-chan T) <-chan T { 30 | forwardPeriod := commonPeriod - period 31 | 32 | if forwardPeriod > 0 { 33 | c = Skip(c, forwardPeriod) 34 | } 35 | 36 | return c 37 | } 38 | -------------------------------------------------------------------------------- /helper/sync_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | ) 12 | 13 | func TestSync(t *testing.T) { 14 | input1 := helper.Skip(helper.SliceToChan([]int{0, 0, 0, 0, 1, 2, 3, 4}), 4) 15 | input2 := helper.Skip(helper.SliceToChan([]int{0, 0, 1, 2, 3, 4, 5, 6}), 2) 16 | input3 := helper.Skip(helper.SliceToChan([]int{0, 0, 0, 1, 2, 3, 4, 5}), 3) 17 | 18 | commonPeriod := helper.CommonPeriod(4, 2, 3) 19 | 20 | actual1 := helper.SyncPeriod(commonPeriod, 4, input1) 21 | expected1 := helper.SliceToChan([]int{1, 2, 3, 4}) 22 | 23 | actual2 := helper.SyncPeriod(commonPeriod, 2, input2) 24 | expected2 := helper.SliceToChan([]int{3, 4, 5, 6}) 25 | 26 | actual3 := helper.SyncPeriod(commonPeriod, 3, input3) 27 | expected3 := helper.SliceToChan([]int{2, 3, 4, 5}) 28 | 29 | err := helper.CheckEquals(actual1, expected1, actual2, expected2, actual3, expected3) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /helper/testdata/with_header.csv: -------------------------------------------------------------------------------- 1 | Date,Asset,Open,Close 2 | "2023-11-26 00:00:00","SP500",10.2,30.4 -------------------------------------------------------------------------------- /helper/testdata/without_header.csv: -------------------------------------------------------------------------------- 1 | "2023-11-26 00:00:00","SP500",10.2,30.4 -------------------------------------------------------------------------------- /helper/waitable.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper 6 | 7 | import "sync" 8 | 9 | // Waitable increments the wait group before reading from the channel 10 | // and signals completion when the channel is closed. 11 | func Waitable[T any](wg *sync.WaitGroup, c <-chan T) <-chan T { 12 | result := make(chan T, cap(c)) 13 | 14 | wg.Add(1) 15 | 16 | go func() { 17 | defer close(result) 18 | defer wg.Done() 19 | 20 | for n := range c { 21 | result <- n 22 | } 23 | }() 24 | 25 | return result 26 | } 27 | -------------------------------------------------------------------------------- /helper/waitable_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package helper_test 6 | 7 | import ( 8 | "sync" 9 | "testing" 10 | 11 | "github.com/cinar/indicator/v2/helper" 12 | ) 13 | 14 | func TestWaitable(_ *testing.T) { 15 | wg := &sync.WaitGroup{} 16 | c := make(chan int) 17 | 18 | helper.Waitable[int](wg, c) 19 | close(c) 20 | 21 | wg.Wait() 22 | } 23 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cinar/indicator/8812266822790ab47aed8c2d70116baf2477e3f3/logo.png -------------------------------------------------------------------------------- /momentum/awesome_oscillator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package momentum_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/momentum" 12 | ) 13 | 14 | func TestAwesomeOscillator(t *testing.T) { 15 | type Data struct { 16 | High float64 17 | Low float64 18 | Ao float64 19 | } 20 | 21 | input, err := helper.ReadFromCsvFile[Data]("testdata/awesome_oscillator.csv", true) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | inputs := helper.Duplicate(input, 3) 27 | highs := helper.Map(inputs[0], func(d *Data) float64 { return d.High }) 28 | lows := helper.Map(inputs[1], func(d *Data) float64 { return d.Low }) 29 | expected := helper.Map(inputs[2], func(d *Data) float64 { return d.Ao }) 30 | 31 | ao := momentum.NewAwesomeOscillator[float64]() 32 | actual := ao.Compute(highs, lows) 33 | actual = helper.RoundDigits(actual, 2) 34 | 35 | expected = helper.Skip(expected, ao.IdlePeriod()) 36 | 37 | err = helper.CheckEquals(actual, expected) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /momentum/momentum.go: -------------------------------------------------------------------------------- 1 | // Package momentum contains the momentum indicator functions. 2 | // 3 | // This package belongs to the Indicator project. Indicator is 4 | // a Golang module that supplies a variety of technical 5 | // indicators, strategies, and a backtesting framework 6 | // for analysis. 7 | // 8 | // # License 9 | // 10 | // Copyright (c) 2021-2024 Onur Cinar. 11 | // The source code is provided under GNU AGPLv3 License. 12 | // https://github.com/cinar/indicator 13 | // 14 | // # Disclaimer 15 | // 16 | // The information provided on this project is strictly for 17 | // informational purposes and is not to be construed as 18 | // advice or solicitation to buy or sell any security. 19 | package momentum 20 | -------------------------------------------------------------------------------- /momentum/qstick_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package momentum_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/momentum" 12 | ) 13 | 14 | func TestQstick(t *testing.T) { 15 | type Data struct { 16 | Open float64 17 | Close float64 18 | Qstick float64 19 | } 20 | 21 | input, err := helper.ReadFromCsvFile[Data]("testdata/qstick.csv", true) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | inputs := helper.Duplicate(input, 3) 27 | openings := helper.Map(inputs[0], func(d *Data) float64 { return d.Open }) 28 | closings := helper.Map(inputs[1], func(d *Data) float64 { return d.Close }) 29 | expected := helper.Map(inputs[2], func(d *Data) float64 { return d.Qstick }) 30 | 31 | qstick := momentum.NewQstick[float64]() 32 | actual := qstick.Compute(openings, closings) 33 | actual = helper.RoundDigits(actual, 2) 34 | 35 | expected = helper.Skip(expected, qstick.IdlePeriod()) 36 | 37 | err = helper.CheckEquals(actual, expected) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /momentum/rsi_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package momentum_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/momentum" 12 | ) 13 | 14 | func TestRsi(t *testing.T) { 15 | type Data struct { 16 | Close float64 17 | Rsi float64 18 | } 19 | 20 | input, err := helper.ReadFromCsvFile[Data]("testdata/rsi.csv", true) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | inputs := helper.Duplicate(input, 2) 26 | closings := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) 27 | expectedRsi := helper.Map(inputs[1], func(d *Data) float64 { return d.Rsi }) 28 | 29 | rsi := momentum.NewRsi[float64]() 30 | actualRsi := rsi.Compute(closings) 31 | actualRsi = helper.RoundDigits(actualRsi, 2) 32 | 33 | expectedRsi = helper.Skip(expectedRsi, rsi.IdlePeriod()) 34 | 35 | err = helper.CheckEquals(actualRsi, expectedRsi) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /momentum/stochastic_oscillator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package momentum_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/momentum" 12 | ) 13 | 14 | func TestStochasticOscillator(t *testing.T) { 15 | type Data struct { 16 | High float64 17 | Low float64 18 | Close float64 19 | K float64 20 | D float64 21 | } 22 | 23 | input, err := helper.ReadFromCsvFile[Data]("testdata/stochastic_oscillator.csv", true) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | inputs := helper.Duplicate(input, 5) 29 | highs := helper.Map(inputs[0], func(d *Data) float64 { return d.High }) 30 | lows := helper.Map(inputs[1], func(d *Data) float64 { return d.Low }) 31 | closings := helper.Map(inputs[2], func(d *Data) float64 { return d.Close }) 32 | expectedK := helper.Map(inputs[3], func(d *Data) float64 { return d.K }) 33 | expectedD := helper.Map(inputs[4], func(d *Data) float64 { return d.D }) 34 | 35 | so := momentum.NewStochasticOscillator[float64]() 36 | actualK, actualD := so.Compute(highs, lows, closings) 37 | actualK = helper.RoundDigits(actualK, 2) 38 | actualD = helper.RoundDigits(actualD, 2) 39 | 40 | expectedK = helper.Skip(expectedK, so.IdlePeriod()) 41 | expectedD = helper.Skip(expectedD, so.IdlePeriod()) 42 | 43 | err = helper.CheckEquals(actualK, expectedK, actualD, expectedD) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /momentum/stochastic_rsi_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package momentum_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/momentum" 12 | ) 13 | 14 | func TestStochasticRsi(t *testing.T) { 15 | type Data struct { 16 | Close float64 17 | StochasticRsi float64 18 | } 19 | 20 | input, err := helper.ReadFromCsvFile[Data]("testdata/stochastic_rsi.csv", true) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | inputs := helper.Duplicate(input, 2) 26 | closings := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) 27 | expected := helper.Map(inputs[1], func(d *Data) float64 { return d.StochasticRsi }) 28 | 29 | stochasticRsi := momentum.NewStochasticRsi[float64]() 30 | actual := stochasticRsi.Compute(closings) 31 | actual = helper.RoundDigits(actual, 2) 32 | 33 | expected = helper.Skip(expected, stochasticRsi.IdlePeriod()) 34 | 35 | err = helper.CheckEquals(actual, expected) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /momentum/williams_r_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package momentum_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/momentum" 12 | ) 13 | 14 | func TestWilliamsR(t *testing.T) { 15 | type Data struct { 16 | High float64 17 | Low float64 18 | Close float64 19 | WilliamsR float64 20 | } 21 | 22 | input, err := helper.ReadFromCsvFile[Data]("testdata/williams_r.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | inputs := helper.Duplicate(input, 4) 28 | highs := helper.Map(inputs[0], func(d *Data) float64 { return d.High }) 29 | lows := helper.Map(inputs[1], func(d *Data) float64 { return d.Low }) 30 | closings := helper.Map(inputs[2], func(d *Data) float64 { return d.Close }) 31 | expected := helper.Map(inputs[3], func(d *Data) float64 { return d.WilliamsR }) 32 | 33 | wr := momentum.NewWilliamsR[float64]() 34 | actual := wr.Compute(highs, lows, closings) 35 | actual = helper.RoundDigits(actual, 2) 36 | 37 | expected = helper.Skip(expected, wr.IdlePeriod()) 38 | 39 | err = helper.CheckEquals(actual, expected) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /revive.toml: -------------------------------------------------------------------------------- 1 | ignoreGeneratedHeader = false 2 | severity = "warning" 3 | confidence = 0.8 4 | errorCode = 0 5 | warningCode = 0 6 | 7 | [rule.blank-imports] 8 | [rule.context-as-argument] 9 | [rule.context-keys-type] 10 | [rule.dot-imports] 11 | [rule.error-return] 12 | [rule.error-strings] 13 | [rule.error-naming] 14 | [rule.exported] 15 | [rule.if-return] 16 | [rule.increment-decrement] 17 | [rule.var-naming] 18 | [rule.var-declaration] 19 | [rule.package-comments] 20 | [rule.range] 21 | [rule.receiver-naming] 22 | [rule.time-naming] 23 | [rule.unexported-return] 24 | [rule.indent-error-flow] 25 | [rule.errorf] 26 | [rule.empty-block] 27 | [rule.superfluous-else] 28 | [rule.unused-parameter] 29 | [rule.unreachable-code] 30 | [rule.redefines-builtin-id] 31 | -------------------------------------------------------------------------------- /strategy/buy_and_hold_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package strategy_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | ) 14 | 15 | func TestBuyAndHoldStrategy(t *testing.T) { 16 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/repository/brk-b.csv", true) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/buy_and_hold_strategy.csv", true) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 27 | 28 | bah := strategy.NewBuyAndHoldStrategy() 29 | actual := bah.Compute(snapshots) 30 | 31 | err = helper.CheckEquals(actual, expected) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | } 36 | 37 | func TestBuyAndHoldStrategyReport(t *testing.T) { 38 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/repository/brk-b.csv", true) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | bah := strategy.NewBuyAndHoldStrategy() 44 | 45 | report := bah.Report(snapshots) 46 | 47 | fileName := "buy_and_hold_strategy.html" 48 | defer helper.Remove(t, fileName) 49 | 50 | err = report.WriteToFile(fileName) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /strategy/compound/compound.go: -------------------------------------------------------------------------------- 1 | // Package compound contains the compound strategy functions. 2 | // 3 | // This package belongs to the Indicator project. Indicator is 4 | // a Golang module that supplies a variety of technical 5 | // indicators, strategies, and a backtesting framework 6 | // for analysis. 7 | // 8 | // # License 9 | // 10 | // Copyright (c) 2021-2024 Onur Cinar. 11 | // The source code is provided under GNU AGPLv3 License. 12 | // https://github.com/cinar/indicator 13 | // 14 | // # Disclaimer 15 | // 16 | // The information provided on this project is strictly for 17 | // informational purposes and is not to be construed as 18 | // advice or solicitation to buy or sell any security. 19 | package compound 20 | 21 | import "github.com/cinar/indicator/v2/strategy" 22 | 23 | // AllStrategies returns a slice containing references to all available compound strategies. 24 | func AllStrategies() []strategy.Strategy { 25 | return []strategy.Strategy{ 26 | NewMacdRsiStrategy(), 27 | NewMacdRsiStrategyWith(20, 80), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /strategy/compound/testdata/brk-b.csv: -------------------------------------------------------------------------------- 1 | ../../../asset/testdata/repository/brk-b.csv -------------------------------------------------------------------------------- /strategy/decorator/decorator.go: -------------------------------------------------------------------------------- 1 | // Package decorator contains the decorator strategy functions. 2 | // 3 | // This package belongs to the Indicator project. Indicator is 4 | // a Golang module that supplies a variety of technical 5 | // indicators, strategies, and a backtesting framework 6 | // for analysis. 7 | // 8 | // # License 9 | // 10 | // Copyright (c) 2021-2024 Onur Cinar. 11 | // The source code is provided under GNU AGPLv3 License. 12 | // https://github.com/cinar/indicator 13 | // 14 | // # Disclaimer 15 | // 16 | // The information provided on this project is strictly for 17 | // informational purposes and is not to be construed as 18 | // advice or solicitation to buy or sell any security. 19 | package decorator 20 | -------------------------------------------------------------------------------- /strategy/decorator/testdata/brk-b.csv: -------------------------------------------------------------------------------- 1 | ../../../asset/testdata/repository/brk-b.csv -------------------------------------------------------------------------------- /strategy/momentum/momentum.go: -------------------------------------------------------------------------------- 1 | // Package momentum contains the momentum strategy functions. 2 | // 3 | // This package belongs to the Indicator project. Indicator is 4 | // a Golang module that supplies a variety of technical 5 | // indicators, strategies, and a backtesting framework 6 | // for analysis. 7 | // 8 | // # License 9 | // 10 | // Copyright (c) 2021-2024 Onur Cinar. 11 | // The source code is provided under GNU AGPLv3 License. 12 | // https://github.com/cinar/indicator 13 | // 14 | // # Disclaimer 15 | // 16 | // The information provided on this project is strictly for 17 | // informational purposes and is not to be construed as 18 | // advice or solicitation to buy or sell any security. 19 | package momentum 20 | 21 | import "github.com/cinar/indicator/v2/strategy" 22 | 23 | // AllStrategies returns a slice containing references to all available momentum strategies. 24 | func AllStrategies() []strategy.Strategy { 25 | return []strategy.Strategy{ 26 | NewAwesomeOscillatorStrategy(), 27 | NewRsiStrategy(), 28 | NewStochasticRsiStrategy(), 29 | NewTripleRsiStrategy(), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /strategy/momentum/rsi_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package momentum_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/momentum" 14 | ) 15 | 16 | func TestRsiStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/rsi_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | rsi := momentum.NewRsiStrategy() 30 | actual := rsi.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestRsiStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | rsi := momentum.NewRsiStrategy() 45 | 46 | report := rsi.Report(snapshots) 47 | 48 | fileName := "rsi_strategy.html" 49 | defer helper.Remove(t, fileName) 50 | 51 | err = report.WriteToFile(fileName) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strategy/momentum/testdata/brk-b.csv: -------------------------------------------------------------------------------- 1 | ../../../asset/testdata/repository/brk-b.csv -------------------------------------------------------------------------------- /strategy/outcome.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package strategy 6 | 7 | import "github.com/cinar/indicator/v2/helper" 8 | 9 | // Outcome simulates the potential result of executing the given actions based on the provided values. 10 | func Outcome[T helper.Number](values <-chan T, actions <-chan Action) <-chan float64 { 11 | balance := 1.0 12 | shares := 0.0 13 | 14 | return helper.Operate(values, actions, func(value T, action Action) float64 { 15 | if balance > 0 && action == Buy { 16 | shares = balance / float64(value) 17 | balance = 0 18 | } else if shares > 0 && action == Sell { 19 | balance = shares * float64(value) 20 | shares = 0 21 | } 22 | 23 | return balance + (shares * float64(value)) - 1.0 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /strategy/outcome_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package strategy_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/strategy" 12 | ) 13 | 14 | func TestOutcome(t *testing.T) { 15 | values := helper.SliceToChan([]float64{ 16 | 10, 15, 12, 12, 18, 17 | 20, 22, 25, 24, 20, 18 | }) 19 | 20 | actions := helper.SliceToChan([]strategy.Action{ 21 | strategy.Hold, strategy.Hold, strategy.Buy, strategy.Buy, strategy.Hold, 22 | strategy.Hold, strategy.Hold, strategy.Sell, strategy.Hold, strategy.Hold, 23 | }) 24 | 25 | expected := helper.SliceToChan([]float64{ 26 | 0, 0, 0, 0, 0.5, 27 | 0.67, 0.83, 1.08, 1.08, 1.08, 28 | }) 29 | 30 | actual := helper.RoundDigits(strategy.Outcome(values, actions), 2) 31 | 32 | err := helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /strategy/result.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package strategy 6 | 7 | // Result is only used inside the test cases to facilitate the 8 | // comparison between the actual and expected strategy results. 9 | type Result struct { 10 | Action Action 11 | } 12 | -------------------------------------------------------------------------------- /strategy/testdata/repository/brk-b.csv: -------------------------------------------------------------------------------- 1 | ../../../asset/testdata/repository/brk-b.csv -------------------------------------------------------------------------------- /strategy/testdata/x: -------------------------------------------------------------------------------- 1 | 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 | -------------------------------------------------------------------------------- /strategy/trend/apo_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/trend" 14 | ) 15 | 16 | func TestApoStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/apo_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | apo := trend.NewApoStrategy() 30 | actual := apo.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestApoStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | apo := trend.NewApoStrategy() 45 | 46 | report := apo.Report(snapshots) 47 | 48 | fileName := "apo_strategy.html" 49 | defer helper.Remove(t, fileName) 50 | 51 | err = report.WriteToFile(fileName) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strategy/trend/aroon_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/trend" 14 | ) 15 | 16 | func TestAroonStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/aroon_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | aroon := trend.NewAroonStrategy() 30 | actual := aroon.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestAroonStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | aroon := trend.NewAroonStrategy() 45 | 46 | report := aroon.Report(snapshots) 47 | 48 | fileName := "aroon_strategy.html" 49 | defer helper.Remove(t, fileName) 50 | 51 | err = report.WriteToFile(fileName) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strategy/trend/bop_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/trend" 14 | ) 15 | 16 | func TestBopStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/bop_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | bop := trend.NewBopStrategy() 30 | actual := bop.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestBopStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | bop := trend.NewBopStrategy() 45 | 46 | report := bop.Report(snapshots) 47 | 48 | fileName := "bop_strategy.html" 49 | defer helper.Remove(t, fileName) 50 | 51 | err = report.WriteToFile(fileName) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strategy/trend/cci_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/trend" 14 | ) 15 | 16 | func TestCciStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/cci_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | cci := trend.NewCciStrategy() 30 | actual := cci.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestCciStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | cci := trend.NewCciStrategy() 45 | 46 | report := cci.Report(snapshots) 47 | 48 | fileName := "cci_strategy.html" 49 | defer helper.Remove(t, fileName) 50 | 51 | err = report.WriteToFile(fileName) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strategy/trend/dema_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/trend" 14 | ) 15 | 16 | func TestDemaStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/dema_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | dema := trend.NewDemaStrategy() 30 | actual := dema.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestDemaStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | dema := trend.NewDemaStrategy() 45 | 46 | report := dema.Report(snapshots) 47 | 48 | fileName := "dema_strategy.html" 49 | defer helper.Remove(t, fileName) 50 | 51 | err = report.WriteToFile(fileName) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strategy/trend/envelope_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/trend" 14 | ) 15 | 16 | func TestEnvelopeStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/envelope_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | envelope := trend.NewEnvelopeStrategy() 30 | actual := envelope.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestEnvelopeStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | envelope := trend.NewEnvelopeStrategy() 45 | report := envelope.Report(snapshots) 46 | 47 | fileName := "envelope_strategy.html" 48 | defer helper.Remove(t, fileName) 49 | 50 | err = report.WriteToFile(fileName) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /strategy/trend/kama_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/trend" 14 | ) 15 | 16 | func TestKamaStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/kama_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | kama := trend.NewKamaStrategy() 30 | actual := kama.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestKamaStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | kama := trend.NewKamaStrategy() 45 | 46 | report := kama.Report(snapshots) 47 | 48 | fileName := "kama_strategy.html" 49 | defer helper.Remove(t, fileName) 50 | 51 | err = report.WriteToFile(fileName) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strategy/trend/kdj_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/trend" 14 | ) 15 | 16 | func TestKdjStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/kdj_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | kdj := trend.NewKdjStrategy() 30 | actual := kdj.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestKdjStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | kdj := trend.NewKdjStrategy() 45 | 46 | report := kdj.Report(snapshots) 47 | 48 | fileName := "kdj_strategy.html" 49 | defer helper.Remove(t, fileName) 50 | 51 | err = report.WriteToFile(fileName) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strategy/trend/macd_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/trend" 14 | ) 15 | 16 | func TestMacdStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/macd_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | macd := trend.NewMacdStrategy() 30 | actual := macd.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestMacdStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | macd := trend.NewMacdStrategy() 45 | 46 | report := macd.Report(snapshots) 47 | 48 | fileName := "macd_strategy.html" 49 | defer helper.Remove(t, fileName) 50 | 51 | err = report.WriteToFile(fileName) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strategy/trend/qstick_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/trend" 14 | ) 15 | 16 | func TestQstickStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/qstick_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | qstick := trend.NewQstickStrategy() 30 | actual := qstick.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestQstickStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | qstick := trend.NewQstickStrategy() 45 | 46 | report := qstick.Report(snapshots) 47 | 48 | fileName := "qstick_strategy.html" 49 | defer helper.Remove(t, fileName) 50 | 51 | err = report.WriteToFile(fileName) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strategy/trend/smma_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/trend" 14 | ) 15 | 16 | func TestSmmaStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/smma_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | smma := trend.NewSmmaStrategy() 30 | actual := smma.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestSmmaStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | smma := trend.NewSmmaStrategy() 45 | 46 | report := smma.Report(snapshots) 47 | 48 | fileName := "smma_strategy.html" 49 | defer helper.Remove(t, fileName) 50 | 51 | err = report.WriteToFile(fileName) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strategy/trend/testdata/brk-b.csv: -------------------------------------------------------------------------------- 1 | ../../../asset/testdata/repository/brk-b.csv -------------------------------------------------------------------------------- /strategy/trend/trend.go: -------------------------------------------------------------------------------- 1 | // Package trend contains the trend strategy functions. 2 | // 3 | // This package belongs to the Indicator project. Indicator is 4 | // a Golang module that supplies a variety of technical 5 | // indicators, strategies, and a backtesting framework 6 | // for analysis. 7 | // 8 | // # License 9 | // 10 | // Copyright (c) 2021-2024 Onur Cinar. 11 | // The source code is provided under GNU AGPLv3 License. 12 | // https://github.com/cinar/indicator 13 | // 14 | // # Disclaimer 15 | // 16 | // The information provided on this project is strictly for 17 | // informational purposes and is not to be construed as 18 | // advice or solicitation to buy or sell any security. 19 | package trend 20 | 21 | import "github.com/cinar/indicator/v2/strategy" 22 | 23 | // AllStrategies returns a slice containing references to all available trend strategies. 24 | func AllStrategies() []strategy.Strategy { 25 | return []strategy.Strategy{ 26 | NewAlligatorStrategy(), 27 | NewApoStrategy(), 28 | NewAroonStrategy(), 29 | NewBopStrategy(), 30 | NewCciStrategy(), 31 | NewDemaStrategy(), 32 | NewGoldenCrossStrategy(), 33 | NewKamaStrategy(), 34 | NewKdjStrategy(), 35 | NewMacdStrategy(), 36 | NewQstickStrategy(), 37 | NewSmmaStrategy(), 38 | NewTrimaStrategy(), 39 | NewTripleMovingAverageCrossoverStrategy(), 40 | NewTsiStrategy(), 41 | NewVwmaStrategy(), 42 | NewWeightedCloseStrategy(), 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /strategy/trend/trima_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/trend" 14 | ) 15 | 16 | func TestTrimaStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/trima_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | trima := trend.NewTrimaStrategy() 30 | actual := trima.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestTrimaStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | trima := trend.NewTrimaStrategy() 45 | 46 | report := trima.Report(snapshots) 47 | 48 | fileName := "trima_strategy.html" 49 | defer helper.Remove(t, fileName) 50 | 51 | err = report.WriteToFile(fileName) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strategy/trend/trix_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/trend" 14 | ) 15 | 16 | func TestTrixStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/trix_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | trix := trend.NewTrixStrategy() 30 | actual := trix.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestTrixStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | trix := trend.NewTrixStrategy() 45 | 46 | report := trix.Report(snapshots) 47 | 48 | fileName := "trix_strategy.html" 49 | defer helper.Remove(t, fileName) 50 | 51 | err = report.WriteToFile(fileName) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strategy/trend/tsi_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/trend" 14 | ) 15 | 16 | func TestTsiStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/tsi_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | tsi := trend.NewTsiStrategy() 30 | actual := tsi.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestTsiStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | tsi := trend.NewTsiStrategy() 45 | 46 | report := tsi.Report(snapshots) 47 | 48 | fileName := "tsi_strategy.html" 49 | defer helper.Remove(t, fileName) 50 | 51 | err = report.WriteToFile(fileName) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strategy/trend/vwma_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/trend" 14 | ) 15 | 16 | func TestVwmaStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/vwma_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | vwma := trend.NewVwmaStrategy() 30 | actual := vwma.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestVwmaStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | vwma := trend.NewVwmaStrategy() 45 | 46 | report := vwma.Report(snapshots) 47 | 48 | fileName := "vwma_strategy.html" 49 | defer helper.Remove(t, fileName) 50 | 51 | err = report.WriteToFile(fileName) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strategy/volatility/testdata/brk-b.csv: -------------------------------------------------------------------------------- 1 | ../../../asset/testdata/repository/brk-b.csv -------------------------------------------------------------------------------- /strategy/volume/force_index_strategy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volume_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/asset" 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/strategy" 13 | "github.com/cinar/indicator/v2/strategy/volume" 14 | ) 15 | 16 | func TestForceIndexStrategy(t *testing.T) { 17 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/force_index_strategy.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) 28 | 29 | fis := volume.NewForceIndexStrategy() 30 | actual := fis.Compute(snapshots) 31 | 32 | err = helper.CheckEquals(actual, expected) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestForceIndexStrategyReport(t *testing.T) { 39 | snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | fis := volume.NewForceIndexStrategy() 45 | report := fis.Report(snapshots) 46 | 47 | fileName := "force_index_strategy.html" 48 | defer helper.Remove(t, fileName) 49 | 50 | err = report.WriteToFile(fileName) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /strategy/volume/testdata/brk-b.csv: -------------------------------------------------------------------------------- 1 | ../../../asset/testdata/repository/brk-b.csv -------------------------------------------------------------------------------- /strategy/volume/volume.go: -------------------------------------------------------------------------------- 1 | // Package volume contains the volume strategy functions. 2 | // 3 | // This package belongs to the Indicator project. Indicator is 4 | // a Golang module that supplies a variety of technical 5 | // indicators, strategies, and a backtesting framework 6 | // for analysis. 7 | // 8 | // # License 9 | // 10 | // Copyright (c) 2021-2024 Onur Cinar. 11 | // The source code is provided under GNU AGPLv3 License. 12 | // https://github.com/cinar/indicator 13 | // 14 | // # Disclaimer 15 | // 16 | // The information provided on this project is strictly for 17 | // informational purposes and is not to be construed as 18 | // advice or solicitation to buy or sell any security. 19 | package volume 20 | 21 | import ( 22 | "github.com/cinar/indicator/v2/strategy" 23 | ) 24 | 25 | // AllStrategies returns a slice containing references to all available volume strategies. 26 | func AllStrategies() []strategy.Strategy { 27 | return []strategy.Strategy{ 28 | NewChaikinMoneyFlowStrategy(), 29 | NewEaseOfMovementStrategy(), 30 | NewForceIndexStrategy(), 31 | NewMoneyFlowIndexStrategy(), 32 | NewNegativeVolumeIndexStrategy(), 33 | NewWeightedAveragePriceStrategy(), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | output: 'prefixed' 3 | 4 | tasks: 5 | default: 6 | cmds: 7 | - task: fmt 8 | - task: lint 9 | - task: test 10 | - task: docs 11 | 12 | action: 13 | deps: [lint, test] 14 | 15 | fmt: 16 | cmds: 17 | - go fix ./... 18 | 19 | lint: 20 | cmds: 21 | - go vet ./... 22 | - go run github.com/securego/gosec/v2/cmd/gosec@v2.20.0 ./... 23 | - go run honnef.co/go/tools/cmd/staticcheck@v0.5.1 ./... 24 | - go run github.com/mgechev/revive@v1.3.4 -config=revive.toml ./... 25 | 26 | test: 27 | cmds: 28 | - go test -cover -coverprofile=coverage.out ./... 29 | 30 | docs: 31 | cmds: 32 | - go run github.com/princjef/gomarkdoc/cmd/gomarkdoc@v1.1.0 ./... 33 | 34 | build-tools: 35 | cmds: 36 | - go build -o indicator-backtest cmd/indicator-backtest/main.go 37 | - go build -o indicator-sync cmd/indicator-sync/main.go 38 | -------------------------------------------------------------------------------- /trend/apo_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestApo(t *testing.T) { 15 | type ApoData struct { 16 | Close float64 17 | Apo float64 18 | } 19 | 20 | input, err := helper.ReadFromCsvFile[ApoData]("testdata/apo.csv", true) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | inputs := helper.Duplicate(input, 2) 26 | closing := helper.Map(inputs[0], func(a *ApoData) float64 { return a.Close }) 27 | expected := helper.Map(inputs[1], func(a *ApoData) float64 { return a.Apo }) 28 | 29 | apo := trend.NewApo[float64]() 30 | actual := helper.RoundDigits(apo.Compute(closing), 2) 31 | expected = helper.Skip(expected, apo.SlowPeriod-1) 32 | 33 | err = helper.CheckEquals(actual, expected) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /trend/aroon_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestAroon(t *testing.T) { 15 | type AroonData struct { 16 | High float64 17 | Low float64 18 | Up float64 19 | Down float64 20 | } 21 | 22 | aroon := trend.NewAroon[float64]() 23 | 24 | input, err := helper.ReadFromCsvFile[AroonData]("testdata/aroon.csv", true) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | inputs := helper.Duplicate(input, 4) 30 | high := helper.Map(inputs[0], func(row *AroonData) float64 { return row.High }) 31 | low := helper.Map(inputs[1], func(row *AroonData) float64 { return row.Low }) 32 | expectedUp := helper.Map(inputs[2], func(row *AroonData) float64 { return row.Up }) 33 | expectedDown := helper.Map(inputs[3], func(row *AroonData) float64 { return row.Down }) 34 | 35 | expectedUp = helper.Skip(expectedUp, aroon.Period-1) 36 | expectedDown = helper.Skip(expectedDown, aroon.Period-1) 37 | 38 | actualUp, actualDown := aroon.Compute(high, low) 39 | 40 | err = helper.CheckEquals(actualUp, expectedUp, actualDown, expectedDown) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /trend/bop.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend 6 | 7 | import "github.com/cinar/indicator/v2/helper" 8 | 9 | // Bop gauges the strength of buying and selling forces using 10 | // the Balance of Power (BoP) indicator. A positive BoP value 11 | // suggests an upward trend, while a negative value indicates 12 | // a downward trend. A BoP value of zero implies equilibrium 13 | // between the two forces. 14 | // 15 | // Formula: BOP = (Closing - Opening) / (High - Low) 16 | type Bop[T helper.Number] struct{} 17 | 18 | // NewBop function initializes a new BOP instance 19 | // with the default parameters. 20 | func NewBop[T helper.Number]() *Bop[T] { 21 | return &Bop[T]{} 22 | } 23 | 24 | // Compute processes a channel of open, high, low, and close values, 25 | // computing the BOP for each entry. 26 | func (*Bop[T]) Compute(opening, high, low, closing <-chan T) <-chan T { 27 | return helper.Divide(helper.Subtract(closing, opening), helper.Subtract(high, low)) 28 | } 29 | -------------------------------------------------------------------------------- /trend/bop_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestBop(t *testing.T) { 15 | type BopData struct { 16 | Open float64 17 | High float64 18 | Low float64 19 | Close float64 20 | Bop float64 21 | } 22 | 23 | input, err := helper.ReadFromCsvFile[BopData]("testdata/bop.csv", true) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | inputs := helper.Duplicate(input, 5) 29 | opening := helper.Map(inputs[0], func(row *BopData) float64 { return row.Open }) 30 | high := helper.Map(inputs[1], func(row *BopData) float64 { return row.High }) 31 | low := helper.Map(inputs[2], func(row *BopData) float64 { return row.Low }) 32 | closing := helper.Map(inputs[3], func(row *BopData) float64 { return row.Close }) 33 | expected := helper.Map(inputs[4], func(row *BopData) float64 { return row.Bop }) 34 | 35 | bop := trend.NewBop[float64]() 36 | actual := bop.Compute(opening, high, low, closing) 37 | 38 | actual = helper.RoundDigits(actual, 0) 39 | 40 | err = helper.CheckEquals(actual, expected) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /trend/cci_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestCci(t *testing.T) { 15 | type Data struct { 16 | High float64 17 | Low float64 18 | Close float64 19 | Cci float64 20 | } 21 | 22 | input, err := helper.ReadFromCsvFile[Data]("testdata/cci.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | inputs := helper.Duplicate(input, 4) 28 | high := helper.Map(inputs[0], func(d *Data) float64 { return d.High }) 29 | low := helper.Map(inputs[1], func(d *Data) float64 { return d.Low }) 30 | closing := helper.Map(inputs[2], func(d *Data) float64 { return d.Close }) 31 | expected := helper.Map(inputs[3], func(d *Data) float64 { return d.Cci }) 32 | 33 | cci := trend.NewCci[float64]() 34 | 35 | actual := cci.Compute(high, low, closing) 36 | actual = helper.RoundDigits(actual, 2) 37 | 38 | expected = helper.Skip(expected, cci.IdlePeriod()) 39 | 40 | err = helper.CheckEquals(actual, expected) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /trend/dema_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestDema(t *testing.T) { 15 | input := helper.SliceToChan([]float64{ 16 | 22.27, 22.19, 22.08, 22.17, 22.18, 22.13, 22.23, 22.43, 22.24, 22.29, 17 | 22.15, 22.39, 22.38, 22.61, 23.36, 24.05, 23.75, 23.83, 23.95, 23.63, 18 | 23.82, 23.87, 23.65, 23.19, 23.10, 23.33, 22.68, 23.10, 22.40, 22.17, 19 | 22.15, 22.39, 22.38, 22.61, 23.36, 24.05, 23.75, 23.83, 23.95, 23.63, 20 | 22.27, 22.19, 22.08, 22.17, 22.18, 22.13, 22.23, 22.43, 22.24, 22.29, 21 | 23.82, 23.87, 23.65, 23.19, 23.10, 23.33, 22.68, 23.10, 22.40, 22.17, 22 | }) 23 | 24 | expected := helper.SliceToChan([]float64{ 25 | 22.51, 22.7, 22.88, 23.01, 23.06, 23.08, 23.16, 23.11, 23.15, 23.05, 26 | 22.92, 22.81, 22.74, 22.66, 22.63, 22.74, 22.97, 23.12, 23.27, 23.43, 27 | 23.52, 23.34, 28 | }) 29 | 30 | dema := trend.NewDema[float64]() 31 | actual := dema.Compute(input) 32 | 33 | actual = helper.RoundDigits(actual, 2) 34 | 35 | err := helper.CheckEquals(actual, expected) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /trend/ema_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/trend" 13 | ) 14 | 15 | // Testing against the example at URL: 16 | // https://school.stockcharts.com/doku.php?id=technical_indicators:moving_averages 17 | 18 | func TestEma(t *testing.T) { 19 | input := helper.SliceToChan([]float64{ 20 | 22.27, 22.19, 22.08, 22.17, 22.18, 22.13, 22.23, 22.43, 22.24, 21 | 22.29, 22.15, 22.39, 22.38, 22.61, 23.36, 24.05, 23.75, 23.83, 22 | 23.95, 23.63, 23.82, 23.87, 23.65, 23.19, 23.10, 23.33, 22.68, 23 | 23.10, 22.40, 22.17, 24 | }) 25 | 26 | expected := []float64{ 27 | 22.22, 22.21, 22.24, 22.27, 22.33, 22.52, 22.80, 22.97, 23.13, 28 | 23.28, 23.34, 23.43, 23.51, 23.53, 23.47, 23.40, 23.39, 23.26, 29 | 23.23, 23.08, 22.92, 30 | } 31 | 32 | ema := trend.NewEmaWithPeriod[float64](10) 33 | ema.Smoothing = 2 34 | 35 | actual := helper.ChanToSlice(helper.RoundDigits(ema.Compute(input), 2)) 36 | 37 | if !reflect.DeepEqual(actual, expected) { 38 | t.Fatalf("actual %v expected %v", actual, expected) 39 | } 40 | } 41 | 42 | func TestEmaString(t *testing.T) { 43 | expected := "EMA(10)" 44 | actual := trend.NewEmaWithPeriod[float64](10).String() 45 | 46 | if actual != expected { 47 | t.Fatalf("actual %v expected %v", actual, expected) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /trend/hma_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestHma(t *testing.T) { 15 | type Data struct { 16 | Close float64 17 | Hma float64 18 | } 19 | 20 | input, err := helper.ReadFromCsvFile[Data]("testdata/hma.csv", true) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | inputs := helper.Duplicate(input, 2) 26 | closing := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) 27 | expected := helper.Map(inputs[1], func(d *Data) float64 { return d.Hma }) 28 | 29 | hma := trend.NewHmaWithPeriod[float64](3) 30 | 31 | actual := hma.Compute(closing) 32 | actual = helper.RoundDigits(actual, 2) 33 | 34 | expected = helper.Skip(expected, hma.IdlePeriod()) 35 | 36 | err = helper.CheckEquals(actual, expected) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | } 41 | 42 | func TestHmaString(t *testing.T) { 43 | expected := "HMA(10)" 44 | actual := trend.NewHmaWithPeriod[float64](10).String() 45 | 46 | if actual != expected { 47 | t.Fatalf("actual %v expected %v", actual, expected) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /trend/ma.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend 6 | 7 | import ( 8 | "github.com/cinar/indicator/v2/helper" 9 | ) 10 | 11 | // Ma represents the interface for the Moving Average (MA) indicators. 12 | type Ma[T helper.Number] interface { 13 | // Compute function takes a channel of numbers and computes the MA. 14 | Compute(<-chan T) <-chan T 15 | 16 | // IdlePeriod is the initial period that MA won't yield any results. 17 | IdlePeriod() int 18 | 19 | // String is the string representation of the MA instance. 20 | String() string 21 | } 22 | -------------------------------------------------------------------------------- /trend/macd_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestMacd(t *testing.T) { 15 | type Data struct { 16 | Close float64 17 | Macd float64 18 | Signal float64 19 | } 20 | 21 | input, err := helper.ReadFromCsvFile[Data]("testdata/macd.csv", true) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | inputs := helper.Duplicate(input, 2) 27 | closing := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) 28 | 29 | macd := trend.NewMacd[float64]() 30 | actualMacds, actualSignals := macd.Compute(closing) 31 | 32 | actualMacds = helper.RoundDigits(actualMacds, 2) 33 | actualSignals = helper.RoundDigits(actualSignals, 2) 34 | 35 | inputs[1] = helper.Skip(inputs[1], macd.IdlePeriod()) 36 | 37 | for data := range inputs[1] { 38 | actualMacd := <-actualMacds 39 | actualSignal := <-actualSignals 40 | 41 | if actualMacd != data.Macd { 42 | t.Fatalf("actual %v expected %v", actualMacd, data.Macd) 43 | } 44 | 45 | if actualSignal != data.Signal { 46 | t.Fatalf("actual %v expected %v", actualSignal, data.Signal) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /trend/mass_index_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestMassIndex(t *testing.T) { 15 | type Data struct { 16 | Open float64 17 | Close float64 18 | MassIndex float64 19 | } 20 | 21 | input, err := helper.ReadFromCsvFile[Data]("testdata/mass_index.csv", true) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | inputs := helper.Duplicate(input, 3) 27 | openings := helper.Map(inputs[0], func(d *Data) float64 { return d.Open }) 28 | closings := helper.Map(inputs[1], func(d *Data) float64 { return d.Close }) 29 | expected := helper.Map(inputs[2], func(d *Data) float64 { return d.MassIndex }) 30 | 31 | mi := trend.NewMassIndex[float64]() 32 | 33 | actual := mi.Compute(openings, closings) 34 | actual = helper.RoundDigits(actual, 2) 35 | actual = helper.Shift(actual, mi.IdlePeriod(), 0) 36 | 37 | err = helper.CheckEquals(actual, expected) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /trend/mlr.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend 6 | 7 | import ( 8 | "github.com/cinar/indicator/v2/helper" 9 | ) 10 | 11 | // Mlr represents the configuration parameters for calculating the Moving Linear Regression. 12 | // 13 | // y = mx + b 14 | // 15 | // Example: 16 | // 17 | // mlr := trend.NewMlrWithPeriod[float64](14) 18 | // rs := mlr.Compute(x , y) 19 | type Mlr[T helper.Number] struct { 20 | // Mls is the Moving Least Square instance. 21 | Mls *Mls[T] 22 | } 23 | 24 | // NewMlrWithPeriod function initializes a new MLR instance with the given period. 25 | func NewMlrWithPeriod[T helper.Number](period int) *Mlr[T] { 26 | return &Mlr[T]{ 27 | Mls: NewMlsWithPeriod[T](period), 28 | } 29 | } 30 | 31 | // Compute function takes a channel of numbers and computes the MLR r. 32 | func (m *Mlr[T]) Compute(x, y <-chan T) <-chan T { 33 | xSplice := helper.Duplicate(x, 2) 34 | 35 | ms, bs := m.Mls.Compute(xSplice[0], y) 36 | 37 | xSplice[1] = helper.Skip(xSplice[1], m.Mls.IdlePeriod()) 38 | 39 | r := helper.Add( 40 | helper.Multiply( 41 | ms, 42 | xSplice[1], 43 | ), 44 | bs, 45 | ) 46 | 47 | return r 48 | } 49 | 50 | // IdlePeriod is the initial period that MLR won't yield any results. 51 | func (m *Mlr[T]) IdlePeriod() int { 52 | return m.Mls.IdlePeriod() 53 | } 54 | -------------------------------------------------------------------------------- /trend/mlr_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestMlr(t *testing.T) { 15 | type Data struct { 16 | X float64 17 | Y float64 18 | R float64 19 | } 20 | 21 | input, err := helper.ReadFromCsvFile[Data]("testdata/mlr.csv", true) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | inputs := helper.Duplicate(input, 3) 27 | x := helper.Map(inputs[0], func(d *Data) float64 { return d.X }) 28 | y := helper.Map(inputs[1], func(d *Data) float64 { return d.Y }) 29 | expected := helper.Map(inputs[2], func(d *Data) float64 { return d.R }) 30 | 31 | mlr := trend.NewMlrWithPeriod[float64](4) 32 | 33 | actual := mlr.Compute(x, y) 34 | actual = helper.RoundDigits(actual, 2) 35 | 36 | expected = helper.Skip(expected, mlr.IdlePeriod()) 37 | 38 | err = helper.CheckEquals(actual, expected) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /trend/mls_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestMls(t *testing.T) { 15 | type Data struct { 16 | X float64 17 | Y float64 18 | M float64 19 | B float64 20 | } 21 | 22 | input, err := helper.ReadFromCsvFile[Data]("testdata/mls.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | inputs := helper.Duplicate(input, 4) 28 | x := helper.Map(inputs[0], func(d *Data) float64 { return d.X }) 29 | y := helper.Map(inputs[1], func(d *Data) float64 { return d.Y }) 30 | expectedM := helper.Map(inputs[2], func(d *Data) float64 { return d.M }) 31 | expectedB := helper.Map(inputs[3], func(d *Data) float64 { return d.B }) 32 | 33 | mls := trend.NewMlsWithPeriod[float64](5) 34 | 35 | actualM, actualB := mls.Compute(x, y) 36 | actualM = helper.RoundDigits(actualM, 2) 37 | actualB = helper.RoundDigits(actualB, 2) 38 | 39 | expectedM = helper.Skip(expectedM, mls.IdlePeriod()) 40 | expectedB = helper.Skip(expectedB, mls.IdlePeriod()) 41 | 42 | err = helper.CheckEquals(actualM, expectedM, actualB, expectedB) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /trend/moving_max.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend 6 | 7 | import "github.com/cinar/indicator/v2/helper" 8 | 9 | // MovingMax represents the configuration parameters for calculating the 10 | // Moving Max over the specified period. 11 | // 12 | // Example: 13 | type MovingMax[T helper.Number] struct { 14 | // Time period. 15 | Period int 16 | } 17 | 18 | // NewMovingMax function initializes a new Moving Max instance with the default parameters. 19 | func NewMovingMax[T helper.Number]() *MovingMax[T] { 20 | return &MovingMax[T]{} 21 | } 22 | 23 | // NewMovingMaxWithPeriod function initializes a new Moving Max instance with the given period. 24 | func NewMovingMaxWithPeriod[T helper.Number](period int) *MovingMax[T] { 25 | return &MovingMax[T]{ 26 | Period: period, 27 | } 28 | } 29 | 30 | // Compute function takes a channel of numbers and computes the 31 | // Moving Max over the specified period. 32 | func (m *MovingMax[T]) Compute(c <-chan T) <-chan T { 33 | cs := helper.Duplicate(c, 2) 34 | cs[1] = helper.Shift(cs[1], m.Period, 0) 35 | 36 | bst := helper.NewBst[T]() 37 | 38 | maxs := helper.Operate(cs[0], cs[1], func(c, b T) T { 39 | bst.Insert(c) 40 | bst.Remove(b) 41 | return bst.Max() 42 | }) 43 | 44 | return helper.Skip(maxs, m.Period-1) 45 | } 46 | 47 | // IdlePeriod is the initial period that Mocing Max won't yield any results. 48 | func (m *MovingMax[T]) IdlePeriod() int { 49 | return m.Period - 1 50 | } 51 | -------------------------------------------------------------------------------- /trend/moving_max_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestMovingMax(t *testing.T) { 15 | input := helper.SliceToChan([]int{-10, 20, -4, -5, 1, 5, 8, 10, -20, 4}) 16 | expected := helper.SliceToChan([]int{20, 20, 5, 8, 10, 10, 10}) 17 | 18 | movingMax := trend.NewMovingMaxWithPeriod[int](4) 19 | actual := movingMax.Compute(input) 20 | 21 | err := helper.CheckEquals(actual, expected) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /trend/moving_min.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend 6 | 7 | import "github.com/cinar/indicator/v2/helper" 8 | 9 | // MovingMin represents the configuration parameters for calculating the 10 | // Moving Min over the specified period. 11 | // 12 | // Example: 13 | type MovingMin[T helper.Number] struct { 14 | // Time period. 15 | Period int 16 | } 17 | 18 | // NewMovingMin function initializes a new Moving Min instance with the default parameters. 19 | func NewMovingMin[T helper.Number]() *MovingMin[T] { 20 | return &MovingMin[T]{} 21 | } 22 | 23 | // NewMovingMinWithPeriod function initializes a new Moving Min instance with the given period. 24 | func NewMovingMinWithPeriod[T helper.Number](period int) *MovingMin[T] { 25 | return &MovingMin[T]{ 26 | Period: period, 27 | } 28 | } 29 | 30 | // Compute function takes a channel of numbers and computes the 31 | // Moving Min over the specified period. 32 | func (m *MovingMin[T]) Compute(c <-chan T) <-chan T { 33 | cs := helper.Duplicate(c, 2) 34 | cs[1] = helper.Shift(cs[1], m.Period, 0) 35 | 36 | bst := helper.NewBst[T]() 37 | 38 | mins := helper.Operate(cs[0], cs[1], func(c, b T) T { 39 | bst.Insert(c) 40 | bst.Remove(b) 41 | return bst.Min() 42 | }) 43 | 44 | return helper.Skip(mins, m.Period-1) 45 | } 46 | 47 | // IdlePeriod is the initial period that Mocing Min won't yield any results. 48 | func (m *MovingMin[T]) IdlePeriod() int { 49 | return m.Period - 1 50 | } 51 | -------------------------------------------------------------------------------- /trend/moving_min_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestMovingMin(t *testing.T) { 15 | input := helper.SliceToChan([]int{-10, 20, -4, -5, 1, 5, 8, 10, -20, 4}) 16 | expected := helper.SliceToChan([]int{-10, -5, -5, -5, 1, -20, -20}) 17 | 18 | movingMin := trend.NewMovingMinWithPeriod[int](4) 19 | actual := movingMin.Compute(input) 20 | 21 | err := helper.CheckEquals(actual, expected) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /trend/moving_sum_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestMovingSum(t *testing.T) { 15 | input := helper.SliceToChan([]int{-10, 20, -4, -5, 1, 5, 8, 10, -20, 4}) 16 | expected := helper.SliceToChan([]int{1, 12, -3, 9, 24, 3, 2}) 17 | 18 | sum := trend.NewMovingSum[int]() 19 | sum.Period = 4 20 | 21 | actual := sum.Compute(input) 22 | 23 | err := helper.CheckEquals(actual, expected) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /trend/rma_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestRma(t *testing.T) { 15 | type Data struct { 16 | Close float64 17 | Rma float64 18 | } 19 | 20 | input, err := helper.ReadFromCsvFile[Data]("testdata/rma.csv", true) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | inputs := helper.Duplicate(input, 2) 26 | closing := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) 27 | expected := helper.Map(inputs[1], func(d *Data) float64 { return d.Rma }) 28 | 29 | rma := trend.NewRma[float64]() 30 | rma.Period = 15 31 | 32 | actual := rma.Compute(closing) 33 | actual = helper.RoundDigits(actual, 2) 34 | 35 | expected = helper.Skip(expected, rma.IdlePeriod()) 36 | 37 | err = helper.CheckEquals(actual, expected) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /trend/sma_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/cinar/indicator/v2/helper" 12 | "github.com/cinar/indicator/v2/trend" 13 | ) 14 | 15 | // Testing against the example at URL: 16 | // https://school.stockcharts.com/doku.php?id=technical_indicators:moving_averages 17 | 18 | func TestSma(t *testing.T) { 19 | input := helper.SliceToChan([]float64{ 20 | 22.27, 22.19, 22.08, 22.17, 22.18, 22.13, 22.23, 22.43, 22.24, 21 | 22.29, 22.15, 22.39, 22.38, 22.61, 23.36, 24.05, 23.75, 23.83, 22 | 23.95, 23.63, 23.82, 23.87, 23.65, 23.19, 23.10, 23.33, 22.68, 23 | 23.10, 22.40, 22.17, 24 | }) 25 | 26 | expected := []float64{ 27 | 22.22, 22.21, 22.23, 22.26, 22.30, 22.42, 22.61, 22.77, 22.91, 28 | 23.08, 23.21, 23.38, 23.53, 23.65, 23.71, 23.68, 23.61, 23.50, 29 | 23.43, 23.28, 23.13, 30 | } 31 | 32 | sma := trend.NewSmaWithPeriod[float64](10) 33 | 34 | actual := helper.ChanToSlice(helper.RoundDigits(sma.Compute(input), 2)) 35 | 36 | if !reflect.DeepEqual(actual, expected) { 37 | t.Fatalf("actual %v expected %v", actual, expected) 38 | } 39 | } 40 | 41 | func TestSmaString(t *testing.T) { 42 | expected := "SMA(10)" 43 | actual := trend.NewSmaWithPeriod[float64](10).String() 44 | 45 | if actual != expected { 46 | t.Fatalf("actual %v expected %v", actual, expected) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /trend/smma_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestSmma(t *testing.T) { 15 | type Data struct { 16 | Close float64 17 | Smma float64 18 | } 19 | 20 | input, err := helper.ReadFromCsvFile[Data]("testdata/smma.csv", true) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | inputs := helper.Duplicate(input, 2) 26 | closing := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) 27 | expected := helper.Map(inputs[1], func(d *Data) float64 { return d.Smma }) 28 | 29 | smma := trend.NewSmma[float64]() 30 | 31 | actual := smma.Compute(closing) 32 | actual = helper.RoundDigits(actual, 2) 33 | 34 | expected = helper.Skip(expected, smma.IdlePeriod()) 35 | 36 | err = helper.CheckEquals(actual, expected) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | } 41 | 42 | func TestSmmaString(t *testing.T) { 43 | expected := "SMMA(10)" 44 | actual := trend.NewSmmaWithPeriod[float64](10).String() 45 | 46 | if actual != expected { 47 | t.Fatalf("actual %v expected %v", actual, expected) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /trend/tema_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestTema(t *testing.T) { 15 | type Data struct { 16 | Close float64 17 | Tema float64 18 | } 19 | 20 | input, err := helper.ReadFromCsvFile[Data]("testdata/tema.csv", true) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | inputs := helper.Duplicate(input, 2) 26 | closing := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) 27 | expected := helper.Map(inputs[1], func(d *Data) float64 { return d.Tema }) 28 | 29 | tema := trend.NewTema[float64]() 30 | 31 | actual := tema.Compute(closing) 32 | actual = helper.RoundDigits(actual, 2) 33 | 34 | expected = helper.Skip(expected, tema.IdlePeriod()) 35 | 36 | err = helper.CheckEquals(actual, expected) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /trend/testdata/kama.csv: -------------------------------------------------------------------------------- 1 | Close,Kama 2 | 110.46,0 3 | 109.80,0 4 | 110.17,0 5 | 109.82,0 6 | 110.15,0 7 | 109.31,0 8 | 109.05,0 9 | 107.94,0 10 | 107.76,0 11 | 109.24,0 12 | 109.40,109.24 13 | 108.50,109.22 14 | 107.96,109.12 15 | 108.55,109.10 16 | 108.85,109.09 17 | 110.44,109.12 18 | 109.89,109.14 19 | 110.70,109.28 20 | 110.79,109.44 21 | 110.22,109.46 22 | 110.00,109.47 23 | 109.27,109.46 24 | 106.69,109.39 25 | 107.07,109.32 26 | 107.92,109.29 27 | 107.95,109.18 28 | 107.70,109.08 29 | 107.97,108.95 30 | 106.09,108.42 31 | 106.03,108.02 32 | 107.65,108.00 33 | 109.54,108.01 34 | 110.26,108.26 35 | 110.38,108.48 36 | 111.94,108.91 37 | 113.59,109.67 38 | 113.98,110.49 39 | 113.91,111.11 40 | 112.62,111.46 41 | 112.20,111.61 42 | 111.10,111.57 43 | 110.18,111.55 44 | 111.13,111.54 45 | 111.55,111.54 46 | 112.08,111.55 47 | 111.95,111.57 48 | 111.60,111.57 49 | 111.39,111.55 50 | 112.25,111.56 51 | -------------------------------------------------------------------------------- /trend/testdata/mlr.csv: -------------------------------------------------------------------------------- 1 | X,Y,R 2 | 0,-1,0 3 | 2,5,0 4 | 5,12,0 5 | 7,20,19.14 6 | -------------------------------------------------------------------------------- /trend/testdata/mls.csv: -------------------------------------------------------------------------------- 1 | X,Y,M,B 2 | 1,1,0,0 3 | 2,2,0,0 4 | 3,3,0,0 5 | 4,4,0,0 6 | 2,4,0.85,0.77 7 | 3,5,0.57,2 8 | 5,7,0.92,1.46 9 | 7,10,1.22,0.89 10 | 9,15,1.52,0.3 11 | -------------------------------------------------------------------------------- /trend/trend.go: -------------------------------------------------------------------------------- 1 | // Package trend contains the trend indicator functions. 2 | // 3 | // This package belongs to the Indicator project. Indicator is 4 | // a Golang module that supplies a variety of technical 5 | // indicators, strategies, and a backtesting framework 6 | // for analysis. 7 | // 8 | // # License 9 | // 10 | // Copyright (c) 2021-2024 Onur Cinar. 11 | // The source code is provided under GNU AGPLv3 License. 12 | // https://github.com/cinar/indicator 13 | // 14 | // # Disclaimer 15 | // 16 | // The information provided on this project is strictly for 17 | // informational purposes and is not to be construed as 18 | // advice or solicitation to buy or sell any security. 19 | package trend 20 | -------------------------------------------------------------------------------- /trend/trix_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestTrix(t *testing.T) { 15 | type Data struct { 16 | Close float64 17 | Trix float64 18 | } 19 | 20 | input, err := helper.ReadFromCsvFile[Data]("testdata/trix.csv", true) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | inputs := helper.Duplicate(input, 2) 26 | closing := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) 27 | expected := helper.Map(inputs[1], func(d *Data) float64 { return d.Trix }) 28 | 29 | trix := trend.NewTrix[float64]() 30 | 31 | actual := trix.Compute(closing) 32 | actual = helper.RoundDigits(actual, 4) 33 | 34 | expected = helper.Skip(expected, trix.IdlePeriod()) 35 | 36 | err = helper.CheckEquals(actual, expected) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /trend/tsi_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestTsi(t *testing.T) { 15 | type Data struct { 16 | Close float64 17 | Tsi float64 18 | } 19 | 20 | input, err := helper.ReadFromCsvFile[Data]("testdata/tsi.csv", true) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | inputs := helper.Duplicate(input, 2) 26 | closing := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) 27 | expected := helper.Map(inputs[1], func(d *Data) float64 { return d.Tsi }) 28 | 29 | tsi := trend.NewTsi[float64]() 30 | actual := tsi.Compute(closing) 31 | 32 | actual = helper.RoundDigits(actual, 2) 33 | expected = helper.Skip(expected, tsi.IdlePeriod()) 34 | 35 | err = helper.CheckEquals(actual, expected) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | 41 | func TestTsiString(t *testing.T) { 42 | expected := "TSI(EMA(1),EMA(2))" 43 | actual := trend.NewTsiWith[float64](1, 2).String() 44 | 45 | if actual != expected { 46 | t.Fatalf("actual %v expected %v", actual, expected) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /trend/typical_price.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend 6 | 7 | import ( 8 | "github.com/cinar/indicator/v2/helper" 9 | ) 10 | 11 | // TypicalPrice represents the configuration parameters for calculating the Typical Price. 12 | // It is another approximation of average price for each period and can be used as a 13 | // filter for moving average systems. 14 | // 15 | // Typical Price = (High + Low + Closing) / 3 16 | type TypicalPrice[T helper.Number] struct{} 17 | 18 | // NewTypicalPrice function initializes a new Typical Price instance with the default parameters. 19 | func NewTypicalPrice[T helper.Number]() *TypicalPrice[T] { 20 | return &TypicalPrice[T]{} 21 | } 22 | 23 | // Compute function takes a channel of numbers and computes the Typical Price and the signal line. 24 | func (*TypicalPrice[T]) Compute(high, low, closing <-chan T) <-chan T { 25 | return helper.DivideBy( 26 | helper.Add( 27 | helper.Add(high, low), 28 | closing, 29 | ), 30 | 3, 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /trend/typical_price_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestTypicalPrice(t *testing.T) { 15 | type Data struct { 16 | High float64 17 | Low float64 18 | Close float64 19 | TypicalPrice float64 20 | } 21 | 22 | input, err := helper.ReadFromCsvFile[Data]("testdata/typical_price.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | inputs := helper.Duplicate(input, 4) 28 | high := helper.Map(inputs[0], func(d *Data) float64 { return d.High }) 29 | low := helper.Map(inputs[1], func(d *Data) float64 { return d.Low }) 30 | closing := helper.Map(inputs[2], func(d *Data) float64 { return d.Close }) 31 | expected := helper.Map(inputs[3], func(d *Data) float64 { return d.TypicalPrice }) 32 | 33 | typicalPrice := trend.NewTypicalPrice[float64]() 34 | 35 | actual := typicalPrice.Compute(high, low, closing) 36 | actual = helper.RoundDigits(actual, 2) 37 | 38 | err = helper.CheckEquals(actual, expected) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /trend/vwma.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend 6 | 7 | import ( 8 | "github.com/cinar/indicator/v2/helper" 9 | ) 10 | 11 | const ( 12 | // DefaultVwmaPeriod is the default period for the VWMA. 13 | DefaultVwmaPeriod = 20 14 | ) 15 | 16 | // Vwma represents the configuration parameters for calculating the Volume Weighted Moving Average (VWMA) 17 | // It averages the price data with an emphasis on volume, meaning areas with higher volume will have a 18 | // greater weight. 19 | // 20 | // VWMA = Sum(Price * Volume) / Sum(Volume) 21 | type Vwma[T helper.Number] struct { 22 | // Time period. 23 | Period int 24 | } 25 | 26 | // NewVwma function initializes a new VWMA instance with the default parameters. 27 | func NewVwma[T helper.Number]() *Vwma[T] { 28 | return &Vwma[T]{ 29 | Period: DefaultVwmaPeriod, 30 | } 31 | } 32 | 33 | // Compute function takes a channel of numbers and computes the VWMA and the signal line. 34 | func (v *Vwma[T]) Compute(closing, volume <-chan T) <-chan T { 35 | volumes := helper.Duplicate(volume, 2) 36 | 37 | sum := NewMovingSum[T]() 38 | sum.Period = v.Period 39 | 40 | return helper.Divide( 41 | sum.Compute( 42 | helper.Multiply(closing, volumes[0]), 43 | ), 44 | sum.Compute(volumes[1]), 45 | ) 46 | } 47 | 48 | // IdlePeriod is the initial period that VWMA won't yield any results. 49 | func (v *Vwma[T]) IdlePeriod() int { 50 | return v.Period - 1 51 | } 52 | -------------------------------------------------------------------------------- /trend/vwma_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestVwma(t *testing.T) { 15 | type Data struct { 16 | Close float64 17 | Volume int64 18 | Vwma float64 19 | } 20 | 21 | input, err := helper.ReadFromCsvFile[Data]("testdata/vwma.csv", true) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | inputs := helper.Duplicate(input, 3) 27 | closing := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) 28 | volume := helper.Map(inputs[1], func(d *Data) float64 { return float64(d.Volume) }) 29 | expected := helper.Map(inputs[2], func(d *Data) float64 { return d.Vwma }) 30 | 31 | vwma := trend.NewVwma[float64]() 32 | 33 | actual := vwma.Compute(closing, volume) 34 | actual = helper.RoundDigits(actual, 2) 35 | 36 | expected = helper.Skip(expected, vwma.IdlePeriod()) 37 | 38 | err = helper.CheckEquals(actual, expected) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /trend/weighted_close.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend 6 | 7 | import ( 8 | "github.com/cinar/indicator/v2/helper" 9 | ) 10 | 11 | // WeightedClose represents the parameters for calculating the Weighted Close indicator. 12 | // 13 | // Weighted Close = (High + Low + (Close * 2)) / 4 14 | // 15 | // Example: 16 | // 17 | // weightedClose := trend.NewWeightedClose[float64]() 18 | // result := weightedClose.Compute(highs, lows, closes) 19 | type WeightedClose[T helper.Number] struct { 20 | } 21 | 22 | // NewWeightedClose function initializes a new Weighted Close instance with the default parameters. 23 | func NewWeightedClose[T helper.Number]() *WeightedClose[T] { 24 | return &WeightedClose[T]{} 25 | } 26 | 27 | // Compute function takes a channel of numbers and computes the Weighted Close over the specified period. 28 | func (*WeightedClose[T]) Compute(highs, lows, closes <-chan T) <-chan T { 29 | return helper.Operate3(highs, lows, closes, func(high, low, close T) T { 30 | // Weighted Close = (High + Low + (Close * 2)) / 4 31 | return (high + low + (close * 2)) / 4 32 | }) 33 | } 34 | 35 | // IdlePeriod is the initial period that Weighted Close yield any results. 36 | func (*WeightedClose[T]) IdlePeriod() int { 37 | return 0 38 | } 39 | 40 | // String is the string representation of the Weighted Close. 41 | func (*WeightedClose[T]) String() string { 42 | return "Weighted Close" 43 | } 44 | -------------------------------------------------------------------------------- /trend/wma_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package trend_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/trend" 12 | ) 13 | 14 | func TestWma(t *testing.T) { 15 | type Data struct { 16 | Close float64 17 | Wma float64 18 | } 19 | 20 | input, err := helper.ReadFromCsvFile[Data]("testdata/wma.csv", true) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | inputs := helper.Duplicate(input, 2) 26 | closing := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) 27 | expected := helper.Map(inputs[1], func(d *Data) float64 { return d.Wma }) 28 | 29 | wma := trend.NewWmaWith[float64](3) 30 | 31 | actual := wma.Compute(closing) 32 | actual = helper.RoundDigits(actual, 2) 33 | 34 | expected = helper.Skip(expected, wma.IdlePeriod()) 35 | 36 | err = helper.CheckEquals(actual, expected) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | } 41 | 42 | func TestWmaString(t *testing.T) { 43 | expected := "WMA(10)" 44 | actual := trend.NewWmaWith[float64](10).String() 45 | 46 | if actual != expected { 47 | t.Fatalf("actual %v expected %v", actual, expected) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /volatility/atr_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volatility_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volatility" 12 | ) 13 | 14 | func TestAtr(t *testing.T) { 15 | type Data struct { 16 | High float64 17 | Low float64 18 | Close float64 19 | Atr float64 20 | } 21 | 22 | input, err := helper.ReadFromCsvFile[Data]("testdata/atr.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | inputs := helper.Duplicate(input, 4) 28 | highs := helper.Map(inputs[0], func(d *Data) float64 { return d.High }) 29 | lows := helper.Map(inputs[1], func(d *Data) float64 { return d.Low }) 30 | closings := helper.Map(inputs[2], func(d *Data) float64 { return d.Close }) 31 | expected := helper.Map(inputs[3], func(d *Data) float64 { return d.Atr }) 32 | 33 | atr := volatility.NewAtrWithPeriod[float64](50) 34 | actual := atr.Compute(highs, lows, closings) 35 | actual = helper.RoundDigits(actual, 2) 36 | 37 | expected = helper.Skip(expected, atr.IdlePeriod()) 38 | 39 | err = helper.CheckEquals(actual, expected) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /volatility/bollinger_band_width_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volatility_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volatility" 12 | ) 13 | 14 | func TestBollingerBandWidth(t *testing.T) { 15 | type Data struct { 16 | Close float64 17 | Width float64 18 | } 19 | 20 | input, err := helper.ReadFromCsvFile[Data]("testdata/bollinger_band_width.csv", true) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | inputs := helper.Duplicate(input, 2) 26 | closings := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) 27 | expected := helper.Map(inputs[1], func(d *Data) float64 { return d.Width }) 28 | 29 | bbw := volatility.NewBollingerBandWidth[float64]() 30 | actual := bbw.Compute(closings) 31 | actual = helper.RoundDigits(actual, 2) 32 | 33 | expected = helper.Skip(expected, bbw.IdlePeriod()) 34 | 35 | err = helper.CheckEquals(actual, expected) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /volatility/moving_std_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volatility_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volatility" 12 | ) 13 | 14 | func TestMovingStd(t *testing.T) { 15 | type Data struct { 16 | Close float64 17 | Std float64 18 | } 19 | 20 | input, err := helper.ReadFromCsvFile[Data]("testdata/moving_std.csv", true) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | inputs := helper.Duplicate(input, 2) 26 | closings := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) 27 | expected := helper.Map(inputs[1], func(d *Data) float64 { return d.Std }) 28 | 29 | std := volatility.NewMovingStdWithPeriod[float64](20) 30 | actual := std.Compute(closings) 31 | actual = helper.RoundDigits(actual, 2) 32 | 33 | expected = helper.Skip(expected, std.IdlePeriod()) 34 | 35 | err = helper.CheckEquals(actual, expected) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /volatility/po_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volatility_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volatility" 12 | ) 13 | 14 | func TestPo(t *testing.T) { 15 | type Data struct { 16 | High float64 17 | Low float64 18 | Close float64 19 | Po float64 20 | } 21 | 22 | input, err := helper.ReadFromCsvFile[Data]("testdata/po.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | inputs := helper.Duplicate(input, 4) 28 | highs := helper.Map(inputs[0], func(d *Data) float64 { return d.High }) 29 | lows := helper.Map(inputs[1], func(d *Data) float64 { return d.Low }) 30 | closings := helper.Map(inputs[2], func(d *Data) float64 { return d.Close }) 31 | expected := helper.Map(inputs[3], func(d *Data) float64 { return d.Po }) 32 | 33 | po := volatility.NewPoWithPeriod[float64](50) 34 | actual := po.Compute(highs, lows, closings) 35 | actual = helper.RoundDigits(actual, 2) 36 | 37 | expected = helper.Skip(expected, po.IdlePeriod()) 38 | 39 | err = helper.CheckEquals(actual, expected) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /volatility/super_trend_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volatility_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volatility" 12 | ) 13 | 14 | func TestSuperTrend(t *testing.T) { 15 | type Data struct { 16 | High float64 17 | Low float64 18 | Close float64 19 | SuperTrend float64 20 | } 21 | 22 | input, err := helper.ReadFromCsvFile[Data]("testdata/super_trend.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | inputs := helper.Duplicate(input, 4) 28 | highs := helper.Map(inputs[0], func(d *Data) float64 { return d.High }) 29 | lows := helper.Map(inputs[1], func(d *Data) float64 { return d.Low }) 30 | closings := helper.Map(inputs[2], func(d *Data) float64 { return d.Close }) 31 | expectedSuperTrends := helper.Map(inputs[3], func(d *Data) float64 { return d.SuperTrend }) 32 | 33 | superTrend := volatility.NewSuperTrend[float64]() 34 | actualSuperTrends := superTrend.Compute(highs, lows, closings) 35 | 36 | actualSuperTrends = helper.RoundDigits(actualSuperTrends, 2) 37 | expectedSuperTrends = helper.Skip(expectedSuperTrends, superTrend.IdlePeriod()) 38 | 39 | err = helper.CheckEquals(actualSuperTrends, expectedSuperTrends) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /volatility/ulcer_index_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volatility_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volatility" 12 | ) 13 | 14 | func TestUlcerIndex(t *testing.T) { 15 | type Data struct { 16 | Close float64 17 | UlcerIndex float64 18 | } 19 | 20 | input, err := helper.ReadFromCsvFile[Data]("testdata/ulcer_index.csv", true) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | inputs := helper.Duplicate(input, 2) 26 | closings := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) 27 | expected := helper.Map(inputs[1], func(d *Data) float64 { return d.UlcerIndex }) 28 | 29 | ui := volatility.NewUlcerIndex[float64]() 30 | actual := ui.Compute(closings) 31 | actual = helper.RoundDigits(actual, 2) 32 | 33 | expected = helper.Skip(expected, ui.IdlePeriod()) 34 | 35 | err = helper.CheckEquals(actual, expected) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /volatility/volatility.go: -------------------------------------------------------------------------------- 1 | // Package volatility contains the volatility indicator functions. 2 | // 3 | // This package belongs to the Indicator project. Indicator is 4 | // a Golang module that supplies a variety of technical 5 | // indicators, strategies, and a backtesting framework 6 | // for analysis. 7 | // 8 | // # License 9 | // 10 | // Copyright (c) 2021-2024 Onur Cinar. 11 | // The source code is provided under GNU AGPLv3 License. 12 | // https://github.com/cinar/indicator 13 | // 14 | // # Disclaimer 15 | // 16 | // The information provided on this project is strictly for 17 | // informational purposes and is not to be construed as 18 | // advice or solicitation to buy or sell any security. 19 | package volatility 20 | -------------------------------------------------------------------------------- /volume/ad_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volume_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volume" 12 | ) 13 | 14 | func TestAd(t *testing.T) { 15 | type AdData struct { 16 | High float64 17 | Low float64 18 | Close float64 19 | Volume int64 20 | Ad float64 21 | } 22 | 23 | input, err := helper.ReadFromCsvFile[AdData]("testdata/ad.csv", true) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | inputs := helper.Duplicate(input, 5) 29 | highs := helper.Map(inputs[0], func(m *AdData) float64 { return m.High }) 30 | lows := helper.Map(inputs[1], func(m *AdData) float64 { return m.Low }) 31 | closings := helper.Map(inputs[2], func(m *AdData) float64 { return m.Close }) 32 | volumes := helper.Map(inputs[3], func(m *AdData) float64 { return float64(m.Volume) }) 33 | expected := helper.Map(inputs[4], func(m *AdData) float64 { return m.Ad }) 34 | 35 | ad := volume.NewAd[float64]() 36 | actual := helper.RoundDigits(ad.Compute(highs, lows, closings, volumes), 2) 37 | expected = helper.Skip(expected, ad.IdlePeriod()) 38 | 39 | err = helper.CheckEquals(actual, expected) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /volume/cmf_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volume_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volume" 12 | ) 13 | 14 | func TestCmf(t *testing.T) { 15 | type CmfData struct { 16 | High float64 17 | Low float64 18 | Close float64 19 | Volume int64 20 | Cmf float64 21 | } 22 | 23 | input, err := helper.ReadFromCsvFile[CmfData]("testdata/cmf.csv", true) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | inputs := helper.Duplicate(input, 5) 29 | highs := helper.Map(inputs[0], func(m *CmfData) float64 { return m.High }) 30 | lows := helper.Map(inputs[1], func(m *CmfData) float64 { return m.Low }) 31 | closings := helper.Map(inputs[2], func(m *CmfData) float64 { return m.Close }) 32 | volumes := helper.Map(inputs[3], func(m *CmfData) float64 { return float64(m.Volume) }) 33 | expected := helper.Map(inputs[4], func(m *CmfData) float64 { return m.Cmf }) 34 | 35 | cmf := volume.NewCmf[float64]() 36 | actual := helper.RoundDigits(cmf.Compute(highs, lows, closings, volumes), 2) 37 | expected = helper.Skip(expected, cmf.IdlePeriod()) 38 | 39 | err = helper.CheckEquals(actual, expected) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /volume/emv_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volume_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volume" 12 | ) 13 | 14 | func TestEmv(t *testing.T) { 15 | type EmvData struct { 16 | High float64 17 | Low float64 18 | Volume int64 19 | Emv float64 20 | } 21 | 22 | input, err := helper.ReadFromCsvFile[EmvData]("testdata/emv.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | inputs := helper.Duplicate(input, 4) 28 | highs := helper.Map(inputs[0], func(m *EmvData) float64 { return m.High }) 29 | lows := helper.Map(inputs[1], func(m *EmvData) float64 { return m.Low }) 30 | volumes := helper.Map(inputs[2], func(m *EmvData) float64 { return float64(m.Volume) }) 31 | expected := helper.Map(inputs[3], func(m *EmvData) float64 { return m.Emv }) 32 | 33 | emv := volume.NewEmv[float64]() 34 | actual := helper.RoundDigits(emv.Compute(highs, lows, volumes), 2) 35 | expected = helper.Skip(expected, emv.IdlePeriod()) 36 | 37 | err = helper.CheckEquals(actual, expected) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /volume/fi_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volume_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volume" 12 | ) 13 | 14 | func TestFi(t *testing.T) { 15 | type FiData struct { 16 | Close float64 17 | Volume int64 18 | Fi float64 19 | } 20 | 21 | input, err := helper.ReadFromCsvFile[FiData]("testdata/fi.csv", true) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | inputs := helper.Duplicate(input, 3) 27 | closings := helper.Map(inputs[0], func(m *FiData) float64 { return m.Close }) 28 | volumes := helper.Map(inputs[1], func(m *FiData) float64 { return float64(m.Volume) }) 29 | expected := helper.Map(inputs[2], func(m *FiData) float64 { return m.Fi }) 30 | 31 | fi := volume.NewFi[float64]() 32 | actual := helper.RoundDigits(fi.Compute(closings, volumes), 2) 33 | expected = helper.Skip(expected, fi.IdlePeriod()) 34 | 35 | err = helper.CheckEquals(actual, expected) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /volume/mfi_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volume_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volume" 12 | ) 13 | 14 | func TestMfi(t *testing.T) { 15 | type MfiData struct { 16 | High float64 17 | Low float64 18 | Close float64 19 | Volume int64 20 | Mfi float64 21 | } 22 | 23 | input, err := helper.ReadFromCsvFile[MfiData]("testdata/mfi.csv", true) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | inputs := helper.Duplicate(input, 5) 29 | highs := helper.Map(inputs[0], func(m *MfiData) float64 { return m.High }) 30 | lows := helper.Map(inputs[1], func(m *MfiData) float64 { return m.Low }) 31 | closings := helper.Map(inputs[2], func(m *MfiData) float64 { return m.Close }) 32 | volumes := helper.Map(inputs[3], func(m *MfiData) float64 { return float64(m.Volume) }) 33 | expected := helper.Map(inputs[4], func(m *MfiData) float64 { return m.Mfi }) 34 | 35 | mfi := volume.NewMfi[float64]() 36 | actual := helper.RoundDigits(mfi.Compute(highs, lows, closings, volumes), 2) 37 | expected = helper.Skip(expected, mfi.IdlePeriod()) 38 | 39 | err = helper.CheckEquals(actual, expected) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /volume/mfm_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volume_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volume" 12 | ) 13 | 14 | func TestMfm(t *testing.T) { 15 | type MfmData struct { 16 | High float64 17 | Low float64 18 | Close float64 19 | Mfm float64 20 | } 21 | 22 | input, err := helper.ReadFromCsvFile[MfmData]("testdata/mfm.csv", true) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | inputs := helper.Duplicate(input, 4) 28 | highs := helper.Map(inputs[0], func(m *MfmData) float64 { return m.High }) 29 | lows := helper.Map(inputs[1], func(m *MfmData) float64 { return m.Low }) 30 | closings := helper.Map(inputs[2], func(m *MfmData) float64 { return m.Close }) 31 | expected := helper.Map(inputs[3], func(m *MfmData) float64 { return m.Mfm }) 32 | 33 | mfm := volume.NewMfm[float64]() 34 | actual := helper.RoundDigits(mfm.Compute(highs, lows, closings), 2) 35 | expected = helper.Skip(expected, mfm.IdlePeriod()) 36 | 37 | err = helper.CheckEquals(actual, expected) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /volume/mfv_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volume_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volume" 12 | ) 13 | 14 | func TestMfv(t *testing.T) { 15 | type MfvData struct { 16 | High float64 17 | Low float64 18 | Close float64 19 | Volume int64 20 | Mfv float64 21 | } 22 | 23 | input, err := helper.ReadFromCsvFile[MfvData]("testdata/mfv.csv", true) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | inputs := helper.Duplicate(input, 5) 29 | highs := helper.Map(inputs[0], func(m *MfvData) float64 { return m.High }) 30 | lows := helper.Map(inputs[1], func(m *MfvData) float64 { return m.Low }) 31 | closings := helper.Map(inputs[2], func(m *MfvData) float64 { return m.Close }) 32 | volumes := helper.Map(inputs[3], func(m *MfvData) float64 { return float64(m.Volume) }) 33 | expected := helper.Map(inputs[4], func(m *MfvData) float64 { return m.Mfv }) 34 | 35 | mfv := volume.NewMfv[float64]() 36 | actual := helper.RoundDigits(mfv.Compute(highs, lows, closings, volumes), 2) 37 | expected = helper.Skip(expected, mfv.IdlePeriod()) 38 | 39 | err = helper.CheckEquals(actual, expected) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /volume/nvi_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volume_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volume" 12 | ) 13 | 14 | func TestNvi(t *testing.T) { 15 | type NviData struct { 16 | Close float64 17 | Volume int64 18 | Nvi float64 19 | } 20 | 21 | input, err := helper.ReadFromCsvFile[NviData]("testdata/nvi.csv", true) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | inputs := helper.Duplicate(input, 3) 27 | closings := helper.Map(inputs[0], func(m *NviData) float64 { return m.Close }) 28 | volumes := helper.Map(inputs[1], func(m *NviData) float64 { return float64(m.Volume) }) 29 | expected := helper.Map(inputs[2], func(m *NviData) float64 { return m.Nvi }) 30 | 31 | nvi := volume.NewNvi[float64]() 32 | actual := helper.RoundDigits(nvi.Compute(closings, volumes), 2) 33 | expected = helper.Skip(expected, nvi.IdlePeriod()) 34 | 35 | err = helper.CheckEquals(actual, expected) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /volume/obv_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volume_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volume" 12 | ) 13 | 14 | func TestObv(t *testing.T) { 15 | type ObvData struct { 16 | Close float64 17 | Volume int64 18 | Obv float64 19 | } 20 | 21 | input, err := helper.ReadFromCsvFile[ObvData]("testdata/obv.csv", true) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | inputs := helper.Duplicate(input, 3) 27 | closings := helper.Map(inputs[0], func(m *ObvData) float64 { return m.Close }) 28 | volumes := helper.Map(inputs[1], func(m *ObvData) float64 { return float64(m.Volume) }) 29 | expected := helper.Map(inputs[2], func(m *ObvData) float64 { return m.Obv }) 30 | 31 | obv := volume.NewObv[float64]() 32 | actual := helper.RoundDigits(obv.Compute(closings, volumes), 2) 33 | expected = helper.Skip(expected, obv.IdlePeriod()) 34 | 35 | err = helper.CheckEquals(actual, expected) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /volume/volume.go: -------------------------------------------------------------------------------- 1 | // Package volume contains the volume indicator functions. 2 | // 3 | // This package belongs to the Indicator project. Indicator is 4 | // a Golang module that supplies a variety of technical 5 | // indicators, strategies, and a backtesting framework 6 | // for analysis. 7 | // 8 | // # License 9 | // 10 | // Copyright (c) 2021-2024 Onur Cinar. 11 | // The source code is provided under GNU AGPLv3 License. 12 | // https://github.com/cinar/indicator 13 | // 14 | // # Disclaimer 15 | // 16 | // The information provided on this project is strictly for 17 | // informational purposes and is not to be construed as 18 | // advice or solicitation to buy or sell any security. 19 | package volume 20 | -------------------------------------------------------------------------------- /volume/vpt.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volume 6 | 7 | import ( 8 | "github.com/cinar/indicator/v2/helper" 9 | ) 10 | 11 | // Vpt holds configuration parameters for calculating the Volume Price Trend (VPT). It provides a correlation 12 | // between the volume and the price. 13 | // 14 | // VPT = Previous VPT + (Volume * (Current Closing - Previous Closing) / Previous Closing) 15 | // 16 | // Example: 17 | // 18 | // vpt := volume.NewVpt[float64]() 19 | // result := vpt.Compute(closings, volumes) 20 | type Vpt[T helper.Number] struct{} 21 | 22 | // NewVpt function initializes a new VPT instance with the default parameters. 23 | func NewVpt[T helper.Number]() *Vpt[T] { 24 | return &Vpt[T]{} 25 | } 26 | 27 | // Compute function takes a channel of numbers and computes the VPT. 28 | func (*Vpt[T]) Compute(closings, volumes <-chan T) <-chan T { 29 | ratios := helper.Multiply( 30 | helper.ChangeRatio(closings, 1), 31 | helper.Skip(volumes, 1), 32 | ) 33 | 34 | return helper.MapWithPrevious(ratios, func(previous, current T) T { 35 | return previous + current 36 | }, 0) 37 | } 38 | 39 | // IdlePeriod is the initial period that VPT won't yield any results. 40 | func (*Vpt[T]) IdlePeriod() int { 41 | return 1 42 | } 43 | -------------------------------------------------------------------------------- /volume/vpt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volume_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volume" 12 | ) 13 | 14 | func TestVpt(t *testing.T) { 15 | type VptData struct { 16 | Close float64 17 | Volume int64 18 | Vpt float64 19 | } 20 | 21 | input, err := helper.ReadFromCsvFile[VptData]("testdata/vpt.csv", true) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | inputs := helper.Duplicate(input, 3) 27 | closings := helper.Map(inputs[0], func(m *VptData) float64 { return m.Close }) 28 | volumes := helper.Map(inputs[1], func(m *VptData) float64 { return float64(m.Volume) }) 29 | expected := helper.Map(inputs[2], func(m *VptData) float64 { return m.Vpt }) 30 | 31 | vpt := volume.NewVpt[float64]() 32 | actual := helper.RoundDigits(vpt.Compute(closings, volumes), 2) 33 | expected = helper.Skip(expected, vpt.IdlePeriod()) 34 | 35 | err = helper.CheckEquals(actual, expected) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /volume/vwap_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Onur Cinar. 2 | // The source code is provided under GNU AGPLv3 License. 3 | // https://github.com/cinar/indicator 4 | 5 | package volume_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/cinar/indicator/v2/helper" 11 | "github.com/cinar/indicator/v2/volume" 12 | ) 13 | 14 | func TestVwap(t *testing.T) { 15 | type VwapData struct { 16 | Close float64 17 | Volume int64 18 | Vwap float64 19 | } 20 | 21 | input, err := helper.ReadFromCsvFile[VwapData]("testdata/vwap.csv", true) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | inputs := helper.Duplicate(input, 3) 27 | closings := helper.Map(inputs[0], func(m *VwapData) float64 { return m.Close }) 28 | volumes := helper.Map(inputs[1], func(m *VwapData) float64 { return float64(m.Volume) }) 29 | expected := helper.Map(inputs[2], func(m *VwapData) float64 { return m.Vwap }) 30 | 31 | vwap := volume.NewVwap[float64]() 32 | actual := helper.RoundDigits(vwap.Compute(closings, volumes), 2) 33 | expected = helper.Skip(expected, vwap.IdlePeriod()) 34 | 35 | err = helper.CheckEquals(actual, expected) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | --------------------------------------------------------------------------------