├── .circleci └── config.yml ├── .travis.yml ├── LICENSE.md ├── README.md ├── backtest ├── README.md ├── backtestable.go ├── backtestable_test.go ├── run_backtest.go ├── run_backtest_cancel_all_test.go ├── run_backtest_cancel_test.go ├── run_backtest_long_limit_order_test.go ├── run_backtest_market_order_test.go ├── run_backtest_short_limit_order_test.go ├── strategy.go ├── strategy_cancel.go └── strategy_execute.go ├── example └── backtest.go ├── go.mod ├── go.sum ├── pine ├── memory_test.go ├── ohlc_prop.go ├── ohlcv.go ├── ohlcv_data_source.go ├── ohlcv_series.go ├── ohlcv_series_base.go ├── ohlcv_series_datasource_example_test.go ├── ohlcv_series_datasource_test.go ├── ohlcv_series_example_test.go ├── ohlcv_series_test.go ├── series_arithmetic.go ├── series_atr.go ├── series_atr_test.go ├── series_cci.go ├── series_cci_test.go ├── series_change.go ├── series_change_test.go ├── series_cross.go ├── series_cross_test.go ├── series_crossover.go ├── series_crossover_test.go ├── series_crossunder.go ├── series_crossunder_test.go ├── series_diff_abs.go ├── series_dmi.go ├── series_dmi_test.go ├── series_ema.go ├── series_ema_test.go ├── series_kc.go ├── series_kc_test.go ├── series_macd.go ├── series_macd_test.go ├── series_mfi.go ├── series_mfi_test.go ├── series_ohlcv_attr.go ├── series_ohlcv_attr_test.go ├── series_operate_nil.go ├── series_operation.go ├── series_operation_const.go ├── series_pow.go ├── series_pow_test.go ├── series_rma.go ├── series_rma_test.go ├── series_roc.go ├── series_roc_test.go ├── series_rsi.go ├── series_rsi_test.go ├── series_sma.go ├── series_sma_test.go ├── series_stdev.go ├── series_stdev_test.go ├── series_sum.go ├── series_sum_test.go ├── series_value_when.go ├── series_value_when_test.go ├── series_variance.go ├── series_variance_test.go ├── testdata_ohlcv_no_gap.go ├── value_series.go └── value_series_test.go └── testutil └── generate_time.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | version: 2.1 4 | 5 | orbs: 6 | codecov: codecov/codecov@3.2.3 7 | 8 | # Define a job to be invoked later in a workflow. 9 | # See: https://circleci.com/docs/2.0/configuration-reference/#jobs 10 | jobs: 11 | build: 12 | # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 13 | # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor 14 | docker: 15 | - image: cimg/go:1.20.2 16 | # Add steps to the job 17 | # See: https://circleci.com/docs/2.0/configuration-reference/#steps 18 | steps: 19 | 20 | - checkout 21 | - restore_cache: 22 | keys: 23 | - go-mod-v4-{{ checksum "go.sum" }} 24 | - run: 25 | name: Install Dependencies 26 | command: go get ./... 27 | - save_cache: 28 | key: go-mod-v4-{{ checksum "go.sum" }} 29 | paths: 30 | - "/go/pkg/mod" 31 | - run: 32 | name: Run build 33 | command: go build ./... 34 | - run: 35 | name: Run tests 36 | command: go test -v -race -coverprofile=coverage.out -covermode=atomic ./... 37 | - codecov/upload 38 | 39 | # Invoke jobs via workflows 40 | # See: https://circleci.com/docs/2.0/configuration-reference/#workflows 41 | workflows: 42 | 43 | deploy-workflow: 44 | jobs: 45 | - build -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.10" 5 | 6 | script: go test ./... 7 | 8 | before_install: 9 | - go get -t -v ./... 10 | 11 | script: 12 | - go test -race -coverprofile=coverage.txt -covermode=atomic 13 | 14 | after_success: 15 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Takuto Suzuki 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🍍 Go Pine 2 | 3 | Backtesting and live trading tool written in Golang inspired by PineScript from TradingView. 4 | 5 | [![Build Status](https://dl.circleci.com/status-badge/img/gh/tsuz/go-pine/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/tsuz/go-pine/tree/main) 6 | [![docs godocs](https://img.shields.io/badge/docs-godoc-brightgreen.svg?style=flat)](https://godoc.org/github.com/tsuz/go-pine) 7 | [![codecov](https://codecov.io/gh/tsuz/go-pine/branch/main/graph/badge.svg?token=1EeuK2Ro6F)](https://codecov.io/gh/tsuz/go-pine) 8 | [![Go Report Card](https://goreportcard.com/badge/tsuz/go-pine)](https://goreportcard.com/report/tsuz/go-pine) 9 | [![Maintainability](https://api.codeclimate.com/v1/badges/ba4f05de8cb12c615695/maintainability)](https://codeclimate.com/github/tsuz/go-pine/maintainability) 10 | 11 | 12 | ## 📘 Documentation 13 | 14 | - For core indicators, checkout the [Indicator documentation][3] 15 | - For backtesting, checkout the [Backtest documentation][4] 16 | 17 | 18 | ## 🏎️ Features 19 | 20 | - 15+ Supported indicators and examples and [more coming][5]. 21 | - Supports [PineScript V5 API][1] 22 | - Similar looking syntax to Pinescript, making conversion to Golang easy 23 | - Many examples ([backtest][6], [indicator][7]) 24 | - Tick level analysis 25 | - Backtesting capability 26 | - Predicting indicators (coming soon) 27 | 28 | ## ❤️ Support 29 | 30 | If you like this project, please give it a ⭐️ 31 | 32 | 33 | 34 | ## ⛔️ Warning 35 | 36 | This library is has not been stress tested and is not production ready. 37 | 38 | 39 | [1]: https://www.tradingview.com/pine-script-reference/v5/ 40 | [2]: backtest/README.md 41 | [3]: http://pkg.go.dev/github.com/tsuz/go-pine/pine 42 | [4]: http://pkg.go.dev/github.com/tsuz/go-pine/backtest/ 43 | [5]: https://github.com/tsuz/go-pine/issues/24 44 | [6]: http://pkg.go.dev/github.com/tsuz/go-pine/backtest/#example_BackTestable 45 | [7]: http://pkg.go.dev/github.com/tsuz/go-pine/pine/#pkg-examples 46 | -------------------------------------------------------------------------------- /backtest/README.md: -------------------------------------------------------------------------------- 1 | # Backtest Docs 2 | 3 | ## Order Types 4 | 5 | If an order with the same ID is already pending, it is possible to modify the order. If there is no order with the specified ID, a new order is placed. To deactivate an entry order, the command `strategy.Cancel()` or `strategy.CancelAll()` should be used. 6 | 7 | These order types are supported. 8 | 9 | - Market Order 10 | - Limit Order 11 | 12 | 13 | ### Market Order 14 | 15 | Market order will be executed on the next OHLCV bar. 16 | 17 | ### Limit Order 18 | 19 | Limit order will be placed and open until either it is executed or cancelled. 20 | 21 | 22 | ## Order Entry/Exit States 23 | 24 | On each `OnNextOHLCV` method, `Entry()` and `Exit()` enters and exits a position, respectively. 25 | 26 | ### Entry() 27 | 28 | - Calling `Entry()` without a limit price will execute on the next bar's open 29 | - Calling `Entry()` with a limit price will execute at the limit price if the next bar's low is equal to or below the limit price. The scenario is the same for short orders using the bar's high value. 30 | - Calling `Entry()` with a limit price that doesn't execute on the next bar's low will be an open order until it is executed or canceled. 31 | 32 | 33 | ### Exit() 34 | 35 | - Calling `Exit()` without a limit price will execute on the next bar's open. 36 | 37 | ### Entry() and Exit() 38 | 39 | If an order entry and exit both exists for the same order ID, here is the expected behavior: 40 | 41 | - If entry and exit are both exist, no trades will be executed. 42 | -------------------------------------------------------------------------------- /backtest/backtestable.go: -------------------------------------------------------------------------------- 1 | package backtest 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/tsuz/go-pine/pine" 7 | ) 8 | 9 | type BackTestable interface { 10 | OnNextOHLCV(Strategy, pine.OHLCVSeries, map[string]interface{}) error 11 | } 12 | 13 | type BacktestResult struct { 14 | ClosedOrd []Position 15 | NetProfit float64 16 | PercentProfitable float64 17 | ProfitableTrades int64 18 | TotalClosedTrades int64 19 | } 20 | 21 | // EntryOpts is additional entry options 22 | type EntryOpts struct { 23 | Comment string 24 | 25 | // Limit price is used if this value is non nil. If it's nil, market order is executed 26 | Limit *float64 27 | 28 | OrdID string 29 | Side Side 30 | Stop string 31 | Qty string 32 | } 33 | 34 | // Px generates a non nil float64 35 | func Px(v float64) *float64 { 36 | v2 := &v 37 | return v2 38 | } 39 | 40 | type Side string 41 | 42 | const ( 43 | Long Side = "long" 44 | Short Side = "short" 45 | ) 46 | 47 | type Position struct { 48 | EntryPx float64 49 | ExitPx float64 50 | EntryTime time.Time 51 | ExitTime time.Time 52 | EntrySide Side 53 | OrdID string 54 | } 55 | 56 | func (p Position) Profit() float64 { 57 | switch p.EntrySide { 58 | case Long: 59 | return p.ExitPx / p.EntryPx 60 | case Short: 61 | return p.EntryPx / p.ExitPx 62 | } 63 | return 0 64 | } 65 | 66 | func (b *BacktestResult) CalculateNetProfit() { 67 | start := 1.0 68 | for _, v := range b.ClosedOrd { 69 | p := v.Profit() 70 | start = start * p 71 | } 72 | b.NetProfit = start 73 | } 74 | -------------------------------------------------------------------------------- /backtest/backtestable_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Backtesting is a process of evaluating a trading strategy using historical market data to simulate how the strategy would have performed in the past. 3 | This library provides such capability using the PineScript-like indicators. 4 | */ 5 | package backtest 6 | 7 | import ( 8 | "log" 9 | "time" 10 | 11 | "github.com/tsuz/go-pine/pine" 12 | ) 13 | 14 | type mystrat struct{} 15 | 16 | func (m *mystrat) OnNextOHLCV(strategy Strategy, s pine.OHLCVSeries, state map[string]interface{}) error { 17 | 18 | close := pine.OHLCVAttr(s, pine.OHLCPropClose) 19 | rsi := pine.SMA(close, 2) 20 | macd, _, _ := pine.MACD(close, 12, 26, 9) 21 | stdev := pine.Stdev(close, 24) 22 | ema200 := pine.EMA(close, 200) 23 | 24 | // we haven't seen enough candles to fulfill the lookback period 25 | if rsi.Val() == nil || macd.Val() == nil || stdev.Val() == nil || ema200.Val() == nil { 26 | return nil 27 | } 28 | 29 | if *rsi.Val() < 30 && *macd.Val() < 0 && *ema200.Val() > 0 { 30 | entry1 := EntryOpts{ 31 | Side: Long, 32 | } 33 | strategy.Entry("Buy1", entry1) 34 | } 35 | 36 | if *rsi.Val() > 70 && *macd.Val() > 0 { 37 | strategy.Exit("Buy1") 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func ExampleBackTestable() { 44 | b := &mystrat{} 45 | data := pine.OHLCVTestData(time.Now(), 25, 5*60*1000) 46 | series, _ := pine.NewOHLCVSeries(data) 47 | 48 | res, _ := RunBacktest(series, b) 49 | 50 | log.Printf("TotalClosedTrades %d, PercentProfitable: %.03f, NetProfit: %.03f", res.TotalClosedTrades, res.PercentProfitable, res.NetProfit) 51 | 52 | } 53 | -------------------------------------------------------------------------------- /backtest/run_backtest.go: -------------------------------------------------------------------------------- 1 | package backtest 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/tsuz/go-pine/pine" 6 | ) 7 | 8 | // Runbacktest starts a backtest 9 | func RunBacktest(series pine.OHLCVSeries, b BackTestable) (*BacktestResult, error) { 10 | strategy := NewStrategy() 11 | states := map[string]interface{}{} 12 | 13 | series.GoToFirst() 14 | 15 | for { 16 | if err := b.OnNextOHLCV(strategy, series, states); err != nil { 17 | return nil, errors.Wrap(err, "error calling OnNextOHLCV") 18 | } 19 | next, err := series.Next() 20 | if err != nil { 21 | return nil, errors.Wrap(err, "error next") 22 | } 23 | 24 | if next == nil { 25 | break 26 | } 27 | if err := strategy.Execute(*next); err != nil { 28 | return nil, errors.Wrapf(err, "error executing next: %+v", *next) 29 | } 30 | } 31 | result := strategy.Result() 32 | return &result, nil 33 | } 34 | -------------------------------------------------------------------------------- /backtest/run_backtest_cancel_all_test.go: -------------------------------------------------------------------------------- 1 | package backtest 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/tsuz/go-pine/pine" 9 | ) 10 | 11 | type testCancelAllMystrat struct{} 12 | 13 | func (m *testCancelAllMystrat) OnNextOHLCV(strategy Strategy, s pine.OHLCVSeries, state map[string]interface{}) error { 14 | 15 | close := pine.OHLCVAttr(s, pine.OHLCPropClose) 16 | 17 | if *close.Val() == 14 { 18 | entry1 := EntryOpts{ 19 | Side: Long, 20 | Limit: Px(12), 21 | } 22 | strategy.Entry("Buy1", entry1) 23 | entry2 := EntryOpts{ 24 | Side: Long, 25 | Limit: Px(11), 26 | } 27 | strategy.Entry("Buy2", entry2) 28 | } 29 | 30 | if *close.Val() <= 13 { 31 | strategy.CancelAll() 32 | } 33 | 34 | strategy.Exit("Buy1") 35 | strategy.Exit("Buy2") 36 | 37 | return nil 38 | } 39 | 40 | // TestRunBacktesttestCancelAllOrder tests canceling existing orders 41 | func TestRunBacktesttestCancelAllOrder(t *testing.T) { 42 | b := &testCancelAllMystrat{} 43 | 44 | data := pine.OHLCVTestData(time.Now(), 3, 5*60*1000) 45 | data[0].C = 14 46 | data[1].L = 13 47 | data[1].C = 13 48 | data[2].L = 10 49 | data[2].C = 10 50 | series, _ := pine.NewOHLCVSeries(data) 51 | 52 | res, err := RunBacktest(series, b) 53 | if err != nil { 54 | t.Fatal(errors.Wrap(err, "error runbacktest")) 55 | } 56 | 57 | if res.TotalClosedTrades != 0 { 58 | t.Errorf("Expected total trades to be 0 but got %d", res.TotalClosedTrades) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /backtest/run_backtest_cancel_test.go: -------------------------------------------------------------------------------- 1 | package backtest 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/tsuz/go-pine/pine" 9 | ) 10 | 11 | type testCancelMystrat struct{} 12 | 13 | func (m *testCancelMystrat) OnNextOHLCV(strategy Strategy, s pine.OHLCVSeries, state map[string]interface{}) error { 14 | 15 | close := pine.OHLCVAttr(s, pine.OHLCPropClose) 16 | 17 | if *close.Val() == 14 { 18 | entry1 := EntryOpts{ 19 | Side: Long, 20 | Limit: Px(12), 21 | } 22 | strategy.Entry("Buy1", entry1) 23 | } 24 | 25 | if *close.Val() == 13 { 26 | strategy.Cancel("Buy1") 27 | } 28 | 29 | strategy.Exit("Buy1") 30 | 31 | return nil 32 | } 33 | 34 | // TestRunBacktesttestCancelOrder tests canceling existing orders 35 | func TestRunBacktesttestCancelOrder(t *testing.T) { 36 | b := &testCancelMystrat{} 37 | 38 | data := pine.OHLCVTestData(time.Now(), 3, 5*60*1000) 39 | data[0].C = 14 40 | data[1].L = 13 41 | data[1].C = 13 42 | data[2].L = 12 43 | data[2].C = 13 44 | series, _ := pine.NewOHLCVSeries(data) 45 | 46 | res, err := RunBacktest(series, b) 47 | if err != nil { 48 | t.Fatal(errors.Wrap(err, "error runbacktest")) 49 | } 50 | 51 | if res.TotalClosedTrades != 0 { 52 | t.Errorf("Expected total trades to be 0 but got %d", res.TotalClosedTrades) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /backtest/run_backtest_long_limit_order_test.go: -------------------------------------------------------------------------------- 1 | package backtest 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/tsuz/go-pine/pine" 10 | ) 11 | 12 | type testLongLimitMystrat struct{} 13 | 14 | func (m *testLongLimitMystrat) OnNextOHLCV(strategy Strategy, s pine.OHLCVSeries, state map[string]interface{}) error { 15 | 16 | close := pine.OHLCVAttr(s, pine.OHLCPropClose) 17 | 18 | if *close.Val() < 15 { 19 | 20 | entry1 := EntryOpts{ 21 | Side: Long, 22 | Limit: Px(14.1), 23 | } 24 | 25 | strategy.Entry("Buy1", entry1) 26 | } 27 | 28 | if *close.Val() > 16 { 29 | strategy.Exit("Buy1") 30 | } 31 | 32 | return nil 33 | } 34 | 35 | // TestRunBacktestLongLimitOrderImmediate tests when limit orders are executed immediately on the next candle 36 | func TestRunBacktestLongLimitOrderImmediate(t *testing.T) { 37 | b := &testLongLimitMystrat{} 38 | data := pine.OHLCVTestData(time.Now(), 4, 5*60*1000) 39 | data[0].C = 14.9 40 | data[0].L = 14.6 41 | data[1].C = 16 42 | data[1].L = 14 43 | data[2].C = 16.1 44 | data[3].O = 16.1 45 | data[3].C = 17 46 | series, _ := pine.NewOHLCVSeries(data) 47 | 48 | res, err := RunBacktest(series, b) 49 | if err != nil { 50 | t.Fatal(errors.Wrap(err, "error runbacktest")) 51 | } 52 | 53 | if res.TotalClosedTrades != 1 { 54 | t.Errorf("Expected total trades to be 1 but got %d", res.TotalClosedTrades) 55 | } 56 | if res.PercentProfitable != 1 { 57 | t.Errorf("Expected pct profitable to be 1 but got %+v", res.PercentProfitable) 58 | } 59 | if res.ProfitableTrades != 1 { 60 | t.Errorf("Expected profitable trades to be 1 but got %+v", res.ProfitableTrades) 61 | } 62 | if fmt.Sprintf("%.03f", res.NetProfit) != "1.142" { 63 | t.Errorf("Expected NetProfit to be 1.142 but got %+v", res.NetProfit) 64 | } 65 | } 66 | 67 | // TestRunBacktestLongLimitOrderPersist that limit orders should persist for multiple candles 68 | func TestRunBacktestLongLimitOrderPersist(t *testing.T) { 69 | b := &testLongLimitMystrat{} 70 | data := pine.OHLCVTestData(time.Now(), 5, 5*60*1000) 71 | data[0].C = 14.5 // limit order triggered 72 | data[0].L = 15 73 | data[1].O = 15 74 | data[1].C = 15 75 | data[1].L = 15 76 | data[2].C = 15 77 | data[2].L = 13 // limit order filled 78 | data[3].O = 16.1 79 | data[3].C = 17 80 | data[4].O = 16.1 81 | data[4].C = 17 82 | series, _ := pine.NewOHLCVSeries(data) 83 | 84 | res, err := RunBacktest(series, b) 85 | if err != nil { 86 | t.Fatal(errors.Wrap(err, "error runbacktest")) 87 | } 88 | 89 | if res.TotalClosedTrades != 1 { 90 | t.Errorf("Expected total trades to be 1 but got %d", res.TotalClosedTrades) 91 | } 92 | if res.PercentProfitable != 1 { 93 | t.Errorf("Expected pct profitable to be 1 but got %+v", res.PercentProfitable) 94 | } 95 | if res.ProfitableTrades != 1 { 96 | t.Errorf("Expected profitable trades to be 1 but got %+v", res.ProfitableTrades) 97 | } 98 | if fmt.Sprintf("%.03f", res.NetProfit) != "1.142" { 99 | t.Errorf("Expected NetProfit to be 1.142 but got %+v", res.NetProfit) 100 | } 101 | } 102 | 103 | // TestRunBacktestLongLimitOrderNotPersistAfterExit that limit orders should persist for multiple candles 104 | func TestRunBacktestLongLimitOrderNotPersistAfterExit(t *testing.T) { 105 | b := &testLongLimitMystrat{} 106 | data := pine.OHLCVTestData(time.Now(), 4, 5*60*1000) 107 | data[0].C = 14.5 // <-- limit order triggered 108 | data[0].L = 15 109 | data[1].L = 13 // <-- limit order filled 110 | data[1].C = 17 // <-- exit triggered 111 | data[2].O = 15 // <-- exit filled 112 | data[2].L = 13 // <-- limit order should not be open anymore so no trigger 113 | data[2].C = 15 // <-- limit order should not be open anymore so no trigger 114 | data[3].O = 16.1 // <-- exit triggered 115 | data[3].C = 17 // <-- should not fill 116 | 117 | series, _ := pine.NewOHLCVSeries(data) 118 | 119 | res, err := RunBacktest(series, b) 120 | if err != nil { 121 | t.Fatal(errors.Wrap(err, "error runbacktest")) 122 | } 123 | 124 | if res.TotalClosedTrades != 1 { 125 | t.Errorf("Expected total trades to be 1 but got %d", res.TotalClosedTrades) 126 | } 127 | if res.PercentProfitable != 1 { 128 | t.Errorf("Expected pct profitable to be 1 but got %+v", res.PercentProfitable) 129 | } 130 | if res.ProfitableTrades != 1 { 131 | t.Errorf("Expected profitable trades to be 1 but got %+v", res.ProfitableTrades) 132 | } 133 | if fmt.Sprintf("%.03f", res.NetProfit) != "1.064" { 134 | t.Errorf("Expected NetProfit to be 1.154 but got %+v", res.NetProfit) 135 | } 136 | } 137 | 138 | // TestRunBacktestLongLimitNotExecuted tests limit orders are not executed 139 | func TestRunBacktestLongLimitNotExecuted(t *testing.T) { 140 | b := &testLongLimitMystrat{} 141 | data := pine.OHLCVTestData(time.Now(), 4, 5*60*1000) 142 | data[0].C = 15 143 | data[0].L = 15 144 | data[1].C = 16 145 | data[1].L = 16 146 | data[2].C = 17 147 | data[2].L = 17 148 | data[3].C = 14.3 149 | data[3].L = 17 150 | 151 | series, _ := pine.NewOHLCVSeries(data) 152 | 153 | res, err := RunBacktest(series, b) 154 | if err != nil { 155 | t.Fatal(errors.Wrap(err, "error runbacktest")) 156 | } 157 | 158 | if res.TotalClosedTrades != 0 { 159 | t.Errorf("Expected total trades to be 0 but got %d", res.TotalClosedTrades) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /backtest/run_backtest_market_order_test.go: -------------------------------------------------------------------------------- 1 | package backtest 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/tsuz/go-pine/pine" 11 | ) 12 | 13 | type testMystrat struct{} 14 | 15 | func (m *testMystrat) OnNextOHLCV(strategy Strategy, s pine.OHLCVSeries, state map[string]interface{}) error { 16 | 17 | close := pine.OHLCVAttr(s, pine.OHLCPropClose) 18 | avg := pine.SMA(close, 2) 19 | 20 | if avg.Val() != nil { 21 | log.Printf("*avg.Val() is %+v", *avg.Val()) 22 | if *avg.Val() > 15.4 { 23 | entry1 := EntryOpts{ 24 | Side: Long, 25 | } 26 | strategy.Entry("Buy1", entry1) 27 | } 28 | if *avg.Val() >= 16.0 { 29 | strategy.Exit("Buy1") 30 | } 31 | } 32 | 33 | return nil 34 | } 35 | 36 | // TestRunBacktestMarketOrder tests when backtest orders are executed using market orders 37 | func TestRunBacktestMarketOrder(t *testing.T) { 38 | b := &testMystrat{} 39 | data := pine.OHLCVTestData(time.Now(), 4, 5*60*1000) 40 | data[0].C = 15 41 | data[1].C = 16 42 | data[2].O = 15.04 43 | data[2].C = 17 44 | data[3].O = 16.92 45 | data[3].C = 18 46 | series, _ := pine.NewOHLCVSeries(data) 47 | 48 | res, err := RunBacktest(series, b) 49 | if err != nil { 50 | t.Fatal(errors.Wrap(err, "error runbacktest")) 51 | } 52 | 53 | if res.TotalClosedTrades != 1 { 54 | t.Errorf("Expected total trades to be 1 but got %d", res.TotalClosedTrades) 55 | } 56 | if res.PercentProfitable != 1 { 57 | t.Errorf("Expected pct profitable to be 1 but got %+v", res.PercentProfitable) 58 | } 59 | if res.ProfitableTrades != 1 { 60 | t.Errorf("Expected profitable trades to be 1 but got %+v", res.ProfitableTrades) 61 | } 62 | if fmt.Sprintf("%.03f", res.NetProfit) != "1.125" { 63 | t.Errorf("Expected NetProfit to be 1.125 but got %+v", res.NetProfit) 64 | } 65 | } 66 | 67 | // TestRunBacktestMarketCancelOutOrder tests when a market entry order and a market exit order are both queued 68 | func TestRunBacktestMarketCancelOutOrder(t *testing.T) { 69 | b := &testMystrat{} 70 | data := pine.OHLCVTestData(time.Now(), 4, 5*60*1000) 71 | data[0].C = 16 72 | data[1].C = 16 73 | data[2].C = 16 74 | data[3].C = 16 75 | 76 | series, _ := pine.NewOHLCVSeries(data) 77 | 78 | res, err := RunBacktest(series, b) 79 | if err != nil { 80 | t.Fatal(errors.Wrap(err, "error runbacktest")) 81 | } 82 | 83 | if res.TotalClosedTrades != 0 { 84 | t.Errorf("Expected total trades to be 0 but got %d", res.TotalClosedTrades) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /backtest/run_backtest_short_limit_order_test.go: -------------------------------------------------------------------------------- 1 | package backtest 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/tsuz/go-pine/pine" 10 | ) 11 | 12 | type testShortLimitMystrat struct{} 13 | 14 | func (m *testShortLimitMystrat) OnNextOHLCV(strategy Strategy, s pine.OHLCVSeries, state map[string]interface{}) error { 15 | 16 | close := pine.OHLCVAttr(s, pine.OHLCPropClose) 17 | 18 | entry1 := EntryOpts{ 19 | Side: Short, 20 | Limit: Px(16.4), 21 | } 22 | 23 | strategy.Entry("Short1", entry1) 24 | 25 | if *close.Val() < 15 { 26 | strategy.Exit("Short1") 27 | } 28 | 29 | return nil 30 | } 31 | 32 | // TestRunBacktestShortLimitOrder tests when backtest orders are executed using Limit orders 33 | func TestRunBacktestShortLimitOrder(t *testing.T) { 34 | b := &testShortLimitMystrat{} 35 | data := pine.OHLCVTestData(time.Now(), 4, 5*60*1000) 36 | data[0].C = 15 37 | data[0].H = 15.2 38 | data[1].C = 16 39 | data[1].H = 16.9 40 | data[2].C = 14 41 | data[2].H = 15.9 42 | data[3].O = 15.8 43 | data[3].C = 16 44 | data[3].H = 16 45 | series, _ := pine.NewOHLCVSeries(data) 46 | 47 | res, err := RunBacktest(series, b) 48 | if err != nil { 49 | t.Fatal(errors.Wrap(err, "error runbacktest")) 50 | } 51 | 52 | if res.TotalClosedTrades != 1 { 53 | t.Errorf("Expected total trades to be 1 but got %d", res.TotalClosedTrades) 54 | } 55 | if res.PercentProfitable != 1 { 56 | t.Errorf("Expected pct profitable to be 1 but got %+v", res.PercentProfitable) 57 | } 58 | if res.ProfitableTrades != 1 { 59 | t.Errorf("Expected profitable trades to be 1 but got %+v", res.ProfitableTrades) 60 | } 61 | if fmt.Sprintf("%.03f", res.NetProfit) != "1.038" { 62 | t.Errorf("Expected NetProfit to be 1.038 but got %+v", res.NetProfit) 63 | } 64 | } 65 | 66 | // TestRunBacktestShortLimitNotExecuted tests limit orders are not executed 67 | func TestRunBacktestShortLimitNotExecuted(t *testing.T) { 68 | b := &testShortLimitMystrat{} 69 | data := pine.OHLCVTestData(time.Now(), 4, 5*60*1000) 70 | data[0].C = 15 71 | data[0].H = 15 72 | data[1].C = 16 73 | data[1].H = 16 74 | data[2].C = 16 75 | data[2].H = 16 76 | data[3].C = 14 77 | data[3].H = 14 78 | 79 | series, _ := pine.NewOHLCVSeries(data) 80 | 81 | res, err := RunBacktest(series, b) 82 | if err != nil { 83 | t.Fatal(errors.Wrap(err, "error runbacktest")) 84 | } 85 | 86 | if res.TotalClosedTrades != 0 { 87 | t.Errorf("Expected total trades to be 0 but got %d", res.TotalClosedTrades) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /backtest/strategy.go: -------------------------------------------------------------------------------- 1 | package backtest 2 | 3 | import ( 4 | "github.com/tsuz/go-pine/pine" 5 | ) 6 | 7 | type Strategy interface { 8 | // Cancel cancels specific order if it's not filled 9 | Cancel(string) error 10 | 11 | // CancelAll cancels all orders 12 | CancelAll() error 13 | 14 | Execute(pine.OHLCV) error 15 | Entry(string, EntryOpts) error 16 | Exit(string) error 17 | Result() BacktestResult 18 | } 19 | 20 | type strategy struct { 21 | openPos map[string]Position 22 | ordEntry map[string]EntryOpts 23 | ordExit map[string]bool 24 | res *BacktestResult 25 | } 26 | 27 | func NewStrategy() Strategy { 28 | s := strategy{ 29 | res: &BacktestResult{}, 30 | openPos: make(map[string]Position), 31 | ordEntry: make(map[string]EntryOpts), 32 | ordExit: make(map[string]bool), 33 | } 34 | return &s 35 | } 36 | 37 | func (s *strategy) deleteEntryOrder(ordID string) { 38 | delete(s.ordEntry, ordID) 39 | } 40 | 41 | func (s *strategy) deleteOpenPos(ordID string) { 42 | delete(s.openPos, ordID) 43 | } 44 | 45 | func (s *strategy) deleteEntryExit(ordID string) { 46 | delete(s.ordExit, ordID) 47 | } 48 | 49 | func (s *strategy) findPos(ordID string) (Position, bool) { 50 | v, ok := s.openPos[ordID] 51 | return v, ok 52 | } 53 | 54 | func (s *strategy) findOrdEntry(ordID string) bool { 55 | _, ok := s.ordEntry[ordID] 56 | return ok 57 | } 58 | 59 | func (s *strategy) setEntryOrder(ordID string, v EntryOpts) { 60 | v.OrdID = ordID 61 | s.ordEntry[ordID] = v 62 | } 63 | 64 | func (s *strategy) setOpenPos(ordID string, v Position) { 65 | s.openPos[ordID] = v 66 | } 67 | 68 | func (s *strategy) setEntryExit(ordID string) { 69 | s.ordExit[ordID] = true 70 | } 71 | 72 | func (s *strategy) Entry(ordID string, opts EntryOpts) error { 73 | s.setEntryOrder(ordID, opts) 74 | return nil 75 | } 76 | 77 | func (s *strategy) completePosition(p Position) { 78 | s.res.ClosedOrd = append(s.res.ClosedOrd, p) 79 | s.res.TotalClosedTrades++ 80 | 81 | prof := p.Profit() 82 | if prof > 0 { 83 | s.res.ProfitableTrades++ 84 | s.res.PercentProfitable = float64(s.res.ProfitableTrades) / float64(s.res.TotalClosedTrades) 85 | } 86 | } 87 | 88 | func (s *strategy) Exit(ordID string) error { 89 | s.setEntryExit(ordID) 90 | return nil 91 | } 92 | 93 | func (s *strategy) Result() BacktestResult { 94 | s.res.CalculateNetProfit() 95 | return *s.res 96 | } 97 | -------------------------------------------------------------------------------- /backtest/strategy_cancel.go: -------------------------------------------------------------------------------- 1 | package backtest 2 | 3 | func (s *strategy) Cancel(ordID string) error { 4 | for _, v := range s.ordEntry { 5 | if v.OrdID == ordID { 6 | s.deleteEntryOrder(ordID) 7 | } 8 | } 9 | 10 | return nil 11 | } 12 | 13 | func (s *strategy) CancelAll() error { 14 | for _, v := range s.ordEntry { 15 | s.deleteEntryOrder(v.OrdID) 16 | } 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /backtest/strategy_execute.go: -------------------------------------------------------------------------------- 1 | package backtest 2 | 3 | import ( 4 | "github.com/tsuz/go-pine/pine" 5 | ) 6 | 7 | func (s *strategy) Execute(ohlcv pine.OHLCV) error { 8 | delFromOrdEntry := make([]string, 0) 9 | 10 | // convert open entry orders into open positions 11 | for _, v := range s.ordEntry { 12 | // if this order is already executed and is open, continue 13 | if _, found := s.findPos(v.OrdID); found { 14 | continue 15 | } 16 | 17 | // if this is also in the exit queue, then cancel this out 18 | if _, found := s.ordExit[v.OrdID]; found { 19 | continue 20 | } 21 | 22 | entryPx := ohlcv.O 23 | 24 | // if limit order, see if it gets filled 25 | if v.Limit != nil { 26 | if v.Side == Long && *v.Limit < ohlcv.L { 27 | // long order not filled 28 | continue 29 | } 30 | if v.Side == Short && *v.Limit > ohlcv.H { 31 | // short order not filled 32 | continue 33 | } 34 | entryPx = *v.Limit 35 | } 36 | 37 | pos := Position{ 38 | EntryPx: entryPx, 39 | EntryTime: ohlcv.S, 40 | EntrySide: v.Side, 41 | OrdID: v.OrdID, 42 | } 43 | s.setOpenPos(v.OrdID, pos) 44 | 45 | delFromOrdEntry = append(delFromOrdEntry, v.OrdID) 46 | } 47 | 48 | for _, v := range delFromOrdEntry { 49 | s.deleteEntryOrder(v) 50 | } 51 | 52 | // convert positions into exit orders 53 | for id := range s.ordExit { 54 | p, found := s.findPos(id) 55 | if found { 56 | p.ExitPx = ohlcv.O 57 | p.ExitTime = ohlcv.S 58 | s.completePosition(p) 59 | s.deleteOpenPos(id) 60 | } 61 | } 62 | 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /example/backtest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/tsuz/go-pine/backtest" 8 | "github.com/tsuz/go-pine/pine" 9 | ) 10 | 11 | type mystrat struct{} 12 | 13 | func (m *mystrat) OnNextOHLCV(strategy backtest.Strategy, s pine.OHLCVSeries, state map[string]interface{}) error { 14 | 15 | var short int64 = 2 16 | var long int64 = 20 17 | var span float64 = 10 18 | 19 | close := pine.OHLCVAttr(s, pine.OHLCPropClose) 20 | open := pine.OHLCVAttr(s, pine.OHLCPropOpen) 21 | 22 | basis := pine.SMA(close, short) 23 | basis2 := pine.SMA(open, long) 24 | rsi := pine.RSI(close, short) 25 | avg := pine.SMA(rsi, long) 26 | 27 | basis3 := pine.Add(basis, basis2) 28 | upperBB := pine.AddConst(basis3, span) 29 | 30 | log.Printf("t: %+v, close: %+v, rsi: %+v, avg: %+v, upperBB: %+v", s.Current().S, close.Val(), rsi.Val(), avg.Val(), upperBB.Val()) 31 | 32 | if rsi.Val() != nil { 33 | if *rsi.Val() < 30 { 34 | log.Printf("Entry: %+v", *rsi.Val()) 35 | entry1 := backtest.EntryOpts{ 36 | Side: backtest.Long, 37 | } 38 | strategy.Entry("Buy1", entry1) 39 | } 40 | if *rsi.Val() > 70 { 41 | log.Printf("Exit %+v", *rsi.Val()) 42 | strategy.Exit("Buy1") 43 | } 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func main() { 50 | b := &mystrat{} 51 | data := pine.OHLCVTestData(time.Now(), 25, 5*60*1000) 52 | series, _ := pine.NewOHLCVSeries(data) 53 | 54 | res, _ := backtest.RunBacktest(series, b) 55 | log.Printf("TotalClosedTrades %d, PercentProfitable: %.03f, NetProfit: %.03f", res.TotalClosedTrades, res.PercentProfitable, res.NetProfit) 56 | } 57 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tsuz/go-pine 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/jinzhu/now v1.1.5 7 | github.com/pkg/errors v0.9.1 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | ) 14 | 15 | require ( 16 | github.com/myesui/uuid v1.0.0 // indirect 17 | github.com/twinj/uuid v1.0.0 18 | gopkg.in/stretchr/testify.v1 v1.2.2 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 4 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 5 | github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI= 6 | github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84= 7 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 8 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk= 12 | github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY= 13 | gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M= 14 | gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU= 15 | -------------------------------------------------------------------------------- /pine/memory_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | _ "net/http/pprof" 5 | "runtime" 6 | "testing" 7 | 8 | "time" 9 | 10 | "github.com/jinzhu/now" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type ds struct{} 15 | 16 | func (d *ds) Populate(t time.Time) ([]OHLCV, error) { 17 | itvl := time.Minute 18 | next := t.Add(itvl) 19 | nextend := next.Add(itvl * 100) 20 | 21 | data := OHLCVTestData(nextend, 100, 60*1000) 22 | return data, nil 23 | } 24 | 25 | type testIndicator = func(o OHLCVSeries) error 26 | 27 | func testMemoryLeak(t *testing.T, fn testIndicator) { 28 | 29 | tn := time.Now() 30 | fromTime := now.With(tn).BeginningOfMonth() 31 | 32 | d := &ds{} 33 | ohlcv, err := d.Populate(fromTime) 34 | if err != nil { 35 | t.Fatal(errors.Wrap(err, "error populating")) 36 | } 37 | 38 | s, err := NewDynamicOHLCVSeries(ohlcv, d) 39 | if err != nil { 40 | panic(errors.Wrap(err, "error creating ohlcvseries")) 41 | } 42 | 43 | first := true 44 | v, err := s.Next() 45 | if err != nil { 46 | panic(errors.Wrap(err, "error next")) 47 | } 48 | 49 | i := 0 50 | var start, last uint64 51 | for { 52 | if v == nil && !first { 53 | break 54 | } 55 | if first { 56 | first = false 57 | } 58 | 59 | c := s.Current() 60 | if c == nil { 61 | break 62 | } 63 | 64 | if ierr := fn(s); ierr != nil { 65 | t.Error(errors.Wrap(err, "error getting")) 66 | } 67 | 68 | v, err = s.Next() 69 | if err != nil { 70 | t.Error(errors.Wrap(err, "error next")) 71 | } 72 | i++ 73 | 74 | if i == 100 { 75 | start = getMalloc() 76 | } 77 | 78 | // get last one 79 | if i == 3000 { 80 | last = getMalloc() 81 | break 82 | } 83 | } 84 | 85 | // error if allocated more than 15MB. This may not catch smaller increments of memory leak 86 | if last > start && last-start > 15000 { 87 | t.Errorf("Memory Leak. Memory allocation increased by %d, start: %d, end: %d", last-start, start, last) 88 | } 89 | } 90 | 91 | func getMalloc() uint64 { 92 | var m runtime.MemStats 93 | runtime.ReadMemStats(&m) 94 | 95 | return m.Alloc / 1024 96 | } 97 | -------------------------------------------------------------------------------- /pine/ohlc_prop.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | // OHLCProp is a property of OHLC 4 | type OHLCProp int 5 | 6 | const ( 7 | // OHLCPropClose is the close value of OHLC 8 | OHLCPropClose OHLCProp = iota 9 | // OHLCPropOpen is the open value of OHLC 10 | OHLCPropOpen 11 | // OHLCPropHigh is the high value of OHLC 12 | OHLCPropHigh 13 | // OHLCPropLow is the low value of OHLC 14 | OHLCPropLow 15 | // OHLCPropVolume is the volume value of OHLC 16 | OHLCPropVolume 17 | // OHLCPropHL2 is the midpoint value of OHLC 18 | OHLCPropHL2 19 | // OHLCPropHLC3 is (high + low + close) / 3 of OHLC 20 | OHLCPropHLC3 21 | // OHLCPropTR is true range i.e. max(high - low, abs(high - close[1]), abs(low - close[1])). 22 | OHLCPropTR 23 | 24 | // OHLCPropTR is true range i.e. na(highsrc[1])? highsrc-lowsrc : math.max(math.max(highsrc - lowsrc, math.abs(highsrc - closesrc[1])), math.abs(lowsrc - closesrc[1])). 25 | // If previous bar doesn't exist, it returns high - low 26 | OHLCPropTRHL 27 | ) 28 | -------------------------------------------------------------------------------- /pine/ohlcv.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import "time" 4 | 5 | type OHLCV struct { 6 | O float64 7 | H float64 8 | L float64 9 | C float64 10 | V float64 11 | S time.Time 12 | 13 | prev *OHLCV 14 | next *OHLCV 15 | } 16 | -------------------------------------------------------------------------------- /pine/ohlcv_data_source.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import "time" 4 | 5 | type DataSource interface { 6 | // Populate is called to fetch more data 7 | // t (time.Time) - the last start time of the last existing OHLCV 8 | // 9 | // Populate is triggered when OHLCVSeries has reached the end and there are no next items 10 | // Returning an empty OHLCV list if nothing else to add 11 | Populate(t time.Time) ([]OHLCV, error) 12 | } 13 | -------------------------------------------------------------------------------- /pine/ohlcv_series.go: -------------------------------------------------------------------------------- 1 | /* 2 | Pine represents core indicators written in the PineScript manual V5. 3 | 4 | While this API looks similar to PineScript, keep in mind these design choices while integrating. 5 | 6 | 1. Every indicator is derived from OHLCVSeries. OHLCVSeries contains information about the candle (i.e. OHLCV, true range, mid point etc) and indicators can use these data as its source. 7 | 2. OHLCVSeries does not sort the order of the OHLCV values. The developer is responsible for providing the correct order. 8 | 3. OHLCVSeries does not make assumptions about the time interval. The developer is responsible for specifying OHLCV's time as well as performing data manipulations before hand such as filling in empty intervals. One advantage of this is that each interval can be as small as an execution tick with a varying interval between them. 9 | 4. OHLCV and indicators are in a series, meaning it will attempt to generate all values up to the specified high watermark. It is specified using either SetCurrent(time.Time) or calling Next() in the OHLCVSeries. 10 | 5. OHLCVSeries differentiates OHLCV items by its start time (i.e. time.Time). Ensure all OHLCV have unique time. 11 | */ 12 | package pine 13 | 14 | // OHLCVSeries represents a series of OHLCV type (i.e. open, high, low, close, volume) 15 | type OHLCVSeries interface { 16 | OHLCVBaseSeries 17 | } 18 | 19 | // NewDynamicOHLCVSeries generates a dynamic OHLCV series 20 | func NewDynamicOHLCVSeries(ohlcv []OHLCV, ds DataSource) (OHLCVSeries, error) { 21 | s := NewOHLCVBaseSeries() 22 | 23 | for _, v := range ohlcv { 24 | s.Push(v) 25 | } 26 | 27 | s.RegisterDataSource(ds) 28 | 29 | return s, nil 30 | } 31 | 32 | func NewOHLCVSeries(ohlcv []OHLCV) (OHLCVSeries, error) { 33 | s := NewOHLCVBaseSeries() 34 | 35 | for _, v := range ohlcv { 36 | s.Push(v) 37 | } 38 | 39 | return s, nil 40 | } 41 | -------------------------------------------------------------------------------- /pine/ohlcv_series_base.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/twinj/uuid" 8 | ) 9 | 10 | // OHLCVBaseSeries represents a series of OHLCV type (i.e. open, high, low, close, volume) 11 | type OHLCVBaseSeries interface { 12 | ID() string 13 | 14 | Push(OHLCV) 15 | 16 | Shift() bool 17 | 18 | Len() int 19 | 20 | // Current returns current ohlcv 21 | Current() *OHLCV 22 | 23 | // Get gets the item by time in value series 24 | Get(time.Time) *OHLCV 25 | 26 | // GetFirst returns first value 27 | GetFirst() *OHLCV 28 | 29 | // GoToFirst sets the current value to first and returns that value 30 | GoToFirst() *OHLCV 31 | 32 | // Next moves the pointer to the next one. 33 | // If there is no next item, nil is returned and the pointer does not advance. 34 | // If there is no next item and a data source registered, it will attempt to fetch and append items if there are any 35 | Next() (*OHLCV, error) 36 | 37 | // registers data source for dynamic updates 38 | RegisterDataSource(DataSource) 39 | 40 | // set the maximum number of OHLCV items. This helps prevent high memory usage. 41 | SetMax(int64) 42 | } 43 | 44 | func NewOHLCVBaseSeries() OHLCVBaseSeries { 45 | u := uuid.NewV4() 46 | s := &ohlcvBaseSeries{ 47 | id: u.String(), 48 | max: 1000, // default maximum items 49 | vals: make(map[int64]*OHLCV), 50 | } 51 | return s 52 | } 53 | 54 | type ohlcvBaseSeries struct { 55 | ds DataSource 56 | 57 | // current ohlcv 58 | cur *OHLCV 59 | 60 | id string 61 | 62 | first *OHLCV 63 | 64 | last *OHLCV 65 | 66 | // max number of candles. 0 means no limit. Defaults to 1000 67 | max int64 68 | 69 | vals map[int64]*OHLCV 70 | } 71 | 72 | func (s *ohlcvBaseSeries) Push(o OHLCV) { 73 | s.vals[o.S.Unix()] = &o 74 | if s.last != nil { 75 | o.prev = s.last 76 | s.last.next = &o 77 | } 78 | s.last = &o 79 | if s.first == nil { 80 | s.first = &o 81 | } 82 | s.resize() 83 | } 84 | 85 | func (s *ohlcvBaseSeries) Shift() bool { 86 | if s.first == nil { 87 | return false 88 | } 89 | delete(s.vals, s.first.S.Unix()) 90 | s.first = s.first.next 91 | if s.first != nil { 92 | s.first.prev = nil 93 | } 94 | return true 95 | } 96 | 97 | func (s *ohlcvBaseSeries) Len() int { 98 | return len(s.vals) 99 | } 100 | 101 | func (s *ohlcvBaseSeries) Current() *OHLCV { 102 | return s.cur 103 | } 104 | 105 | func (s *ohlcvBaseSeries) Get(t time.Time) *OHLCV { 106 | return s.getValue(t.Unix()) 107 | } 108 | 109 | func (s *ohlcvBaseSeries) getValue(t int64) *OHLCV { 110 | return s.vals[t] 111 | } 112 | 113 | func (s *ohlcvBaseSeries) GetFirst() *OHLCV { 114 | return s.first 115 | } 116 | 117 | func (s *ohlcvBaseSeries) GoToFirst() *OHLCV { 118 | s.cur = s.first 119 | return s.cur 120 | } 121 | 122 | func (s *ohlcvBaseSeries) ID() string { 123 | return s.id 124 | } 125 | 126 | func (s *ohlcvBaseSeries) fetchAndAppend() (bool, error) { 127 | more, err := s.ds.Populate(s.cur.S) 128 | if err != nil { 129 | return false, errors.Wrap(err, "error populating") 130 | } 131 | for _, v := range more { 132 | s.Push(v) 133 | } 134 | return len(more) > 0, nil 135 | } 136 | 137 | func (s *ohlcvBaseSeries) Next() (*OHLCV, error) { 138 | if s.cur == nil { 139 | if len(s.vals) == 0 { 140 | return nil, nil 141 | } 142 | // set first one if nil 143 | s.cur = s.first 144 | return s.cur, nil 145 | } 146 | if s.cur.next == nil { 147 | if s.ds != nil { 148 | found, err := s.fetchAndAppend() 149 | if err != nil { 150 | return nil, errors.Wrap(err, "error populating") 151 | } 152 | if !found { 153 | return nil, nil 154 | } 155 | return s.Next() 156 | } 157 | return nil, nil 158 | } 159 | s.cur = s.cur.next 160 | return s.cur, nil 161 | } 162 | 163 | func (s *ohlcvBaseSeries) RegisterDataSource(ds DataSource) { 164 | s.ds = ds 165 | } 166 | 167 | func (s *ohlcvBaseSeries) SetMax(m int64) { 168 | 169 | s.max = m 170 | 171 | s.resize() 172 | } 173 | 174 | func (s *ohlcvBaseSeries) resize() { 175 | m := s.max 176 | // set to unlimited, nothing to perform 177 | if m == 0 { 178 | return 179 | } 180 | for { 181 | if int64(s.Len()) <= m { 182 | break 183 | } 184 | s.Shift() 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /pine/ohlcv_series_datasource_example_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "log" 5 | "time" 6 | ) 7 | 8 | type mytestds struct { 9 | data2 []OHLCV 10 | } 11 | 12 | func MyNewTestDynamicDS(data2 []OHLCV) DataSource { 13 | return &testds{data2: data2} 14 | } 15 | 16 | // Populate is triggered when OHLCVSeries has reached the end and there are no next items 17 | // Returning an empty OHLCV list if nothing else to add 18 | func (t *mytestds) Populate(v time.Time) ([]OHLCV, error) { 19 | 20 | // Fetch data from API 21 | if t.data2[0].S.Sub(v) > 0 { 22 | return t.data2, nil 23 | } 24 | 25 | return []OHLCV{}, nil 26 | } 27 | 28 | func ExampleNewDynamicOHLCVSeries() { 29 | start := time.Now() 30 | data := OHLCVTestData(start, 3, 5*60*1000) 31 | data2 := OHLCVTestData(start.Add(3*5*time.Minute), 3, 5*60*1000) 32 | 33 | ds := MyNewTestDynamicDS(data2) 34 | s, _ := NewDynamicOHLCVSeries(data, ds) 35 | 36 | for { 37 | v, _ := s.Next() 38 | if v == nil { 39 | break 40 | } 41 | log.Printf("Close is %+v", v.C) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pine/ohlcv_series_datasource_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type testds struct { 11 | data2 []OHLCV 12 | } 13 | 14 | func NewTestDynamicDS(data2 []OHLCV) DataSource { 15 | return &testds{data2: data2} 16 | } 17 | 18 | func (t *testds) Populate(v time.Time) ([]OHLCV, error) { 19 | if t.data2[0].S.Sub(v) > 0 { 20 | return t.data2, nil 21 | } 22 | return []OHLCV{}, nil 23 | } 24 | 25 | func TestNewOHLCVSeriesFetchDataSource(t *testing.T) { 26 | start := time.Now() 27 | data := OHLCVTestData(start, 3, 5*60*1000) 28 | data2 := OHLCVTestData(start.Add(3*5*time.Minute), 3, 5*60*1000) 29 | 30 | ds := NewTestDynamicDS(data2) 31 | s, err := NewDynamicOHLCVSeries(data, ds) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | for i := 0; i < (len(data) + len(data2)); i++ { 37 | _, err := s.Next() 38 | if err != nil { 39 | t.Fatal(errors.Wrap(err, "error fetching next")) 40 | } 41 | close := OHLCVAttr(s, OHLCPropClose) 42 | if i < len(data) { 43 | if *close.Val() != data[i].C { 44 | t.Errorf("expected %+v but got %+v", data[i].C, *close.Val()) 45 | } 46 | } else { 47 | if *close.Val() != data2[i-len(data)].C { 48 | t.Errorf("expected %+v but got %+v", data2[i-len(data)].C, *close.Val()) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pine/ohlcv_series_example_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "log" 5 | "time" 6 | ) 7 | 8 | func ExampleNewOHLCVSeries() { 9 | start := time.Now() 10 | // start = start time of the first OHLCV bar 11 | // 3 = 3 bars 12 | // 5*60*1000 = 5 minutes in milliseconds 13 | data := OHLCVTestData(start, 3, 5*60*1000) 14 | s, _ := NewOHLCVSeries(data) 15 | 16 | for { 17 | v, _ := s.Next() 18 | if v == nil { 19 | break 20 | } 21 | log.Printf("Close: %+v", v.C) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pine/ohlcv_series_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestNewOHLCVSeriesPush(t *testing.T) { 9 | start := time.Now() 10 | data := OHLCVTestData(start, 3, 5*60*1000) 11 | empty := make([]OHLCV, 0) 12 | s, err := NewOHLCVSeries(empty) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | for i, v := range data { 18 | s.Push(v) 19 | 20 | if s.Len() != i+1 { 21 | t.Errorf("expected len of %d but got %d", i+1, s.Len()) 22 | } 23 | } 24 | 25 | for i := 0; i < 3; i++ { 26 | s.Next() 27 | close := OHLCVAttr(s, OHLCPropClose) 28 | if *close.Val() != data[i].C { 29 | t.Errorf("expected %+v but got %+v", data[i].C, *close.Val()) 30 | } 31 | } 32 | } 33 | 34 | func TestNewOHLCVSeriesShift(t *testing.T) { 35 | start := time.Now() 36 | data := OHLCVTestData(start, 3, 5*60*1000) 37 | 38 | s, err := NewOHLCVSeries(data) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | for i := 0; i < 3; i++ { 44 | s.Shift() 45 | if s.Len() != 3-(i+1) { 46 | t.Errorf("expected len of %d but got %d", 3-(i+1), s.Len()) 47 | } 48 | } 49 | } 50 | 51 | func TestNewOHLCVSeriesMaxResize(t *testing.T) { 52 | start := time.Now() 53 | data := OHLCVTestData(start, 6, 5*60*1000) 54 | 55 | s, err := NewOHLCVSeries(data) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | s.SetMax(3) 60 | 61 | for i := 0; i < 3; i++ { 62 | v, _ := s.Next() 63 | if v.C != data[i+3].C { 64 | t.Errorf("expected %+v but got %+v", v.C, data[i+3].C) 65 | } 66 | } 67 | } 68 | 69 | func TestNewOHLCVSeriesMaxCheckUponPush(t *testing.T) { 70 | start := time.Now() 71 | data := OHLCVTestData(start, 3, 5*60*1000) 72 | newv := OHLCVTestData(start.Add(3*5*time.Minute), 1, 5*60*1000) 73 | 74 | s, err := NewOHLCVSeries(data) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | s.SetMax(3) 79 | 80 | s.Push(newv[0]) 81 | 82 | for i := 0; i < 3; i++ { 83 | v, _ := s.Next() 84 | if i < 2 { 85 | if v.C != data[i+1].C { 86 | t.Errorf("expected %+v but got %+v for %d", data[i+1].C, v.C, i) 87 | } 88 | } else { 89 | if v.C != newv[0].C { 90 | t.Errorf("expected %+v but got %+v for %d", v.C, newv[0].C, i) 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pine/series_arithmetic.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func Add(a, b ValueSeries) ValueSeries { 8 | return operation(a, b, "add", func(av, bv float64) float64 { 9 | return av + bv 10 | }, true) 11 | } 12 | 13 | func AddConst(a ValueSeries, c float64) ValueSeries { 14 | key := fmt.Sprintf("addconst:%+v", c) 15 | return operationConst(a, key, func(av float64) float64 { 16 | return av + c 17 | }, true) 18 | } 19 | 20 | func AddConstNoCache(a ValueSeries, c float64) ValueSeries { 21 | key := fmt.Sprintf("addconst:%+v", c) 22 | return operationConst(a, key, func(av float64) float64 { 23 | return av + c 24 | }, false) 25 | } 26 | 27 | func Copy(a ValueSeries) ValueSeries { 28 | return operation(a, a, "copy", func(av, _ float64) float64 { 29 | return av 30 | }, true) 31 | } 32 | 33 | func Div(a, b ValueSeries) ValueSeries { 34 | return operation(a, b, "div", func(av, bv float64) float64 { 35 | return av / bv 36 | }, true) 37 | } 38 | 39 | func DivNoCache(a, b ValueSeries) ValueSeries { 40 | return operation(a, b, "div", func(av, bv float64) float64 { 41 | return av / bv 42 | }, false) 43 | } 44 | 45 | func DivConst(a ValueSeries, c float64) ValueSeries { 46 | key := fmt.Sprintf("divconst:%+v", c) 47 | return operationConst(a, key, func(av float64) float64 { 48 | return av / c 49 | }, true) 50 | } 51 | 52 | func DivConstNoCache(a ValueSeries, c float64) ValueSeries { 53 | key := fmt.Sprintf("divconst:%+v", c) 54 | return operationConst(a, key, func(av float64) float64 { 55 | return av / c 56 | }, false) 57 | } 58 | 59 | func Mul(a, b ValueSeries) ValueSeries { 60 | return operation(a, b, "mul", func(av, bv float64) float64 { 61 | return av * bv 62 | }, true) 63 | } 64 | 65 | func MulConst(a ValueSeries, c float64) ValueSeries { 66 | key := fmt.Sprintf("mulconst:%+v", c) 67 | return operationConst(a, key, func(av float64) float64 { 68 | return av * c 69 | }, true) 70 | } 71 | 72 | func MulConstNoCache(a ValueSeries, c float64) ValueSeries { 73 | key := fmt.Sprintf("mulconst:%+v", c) 74 | return operationConst(a, key, func(av float64) float64 { 75 | return av * c 76 | }, false) 77 | } 78 | 79 | func ReplaceAll(a ValueSeries, c float64) ValueSeries { 80 | key := fmt.Sprintf("replace:%+v", c) 81 | return operation(a, a, key, func(av, bv float64) float64 { 82 | return c 83 | }, true) 84 | } 85 | 86 | func Sub(a, b ValueSeries) ValueSeries { 87 | return operation(a, b, "sub", func(av, bv float64) float64 { 88 | return av - bv 89 | }, true) 90 | } 91 | 92 | func SubConst(a ValueSeries, c float64) ValueSeries { 93 | key := fmt.Sprintf("subconst:%+v", c) 94 | return operationConst(a, key, func(av float64) float64 { 95 | return av - c 96 | }, true) 97 | } 98 | 99 | func SubConstNoCache(a ValueSeries, c float64) ValueSeries { 100 | key := fmt.Sprintf("subconst:%+v", c) 101 | return operationConst(a, key, func(av float64) float64 { 102 | return av - c 103 | }, false) 104 | } 105 | -------------------------------------------------------------------------------- /pine/series_atr.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | // ATR generates a ValueSeries of average true range 4 | // 5 | // Function ATR returns the RMA of true range ValueSeries. 6 | // True range is already generated by OHLCVSeries. 7 | // True range is 8 | // - max(high - low, abs(high - close[1]), abs(low - close[1])). 9 | // 10 | // The arguments are: 11 | // - tr: ValueSeries - true range value 12 | // - length: int - lookback length to generate ATR. 1 is same as the current value. 13 | // 14 | // The return values are: 15 | // - ATR: ValueSeries - ATR 16 | // - err: error 17 | func ATR(tr ValueSeries, l int64) ValueSeries { 18 | return RMA(tr, l) 19 | } 20 | -------------------------------------------------------------------------------- /pine/series_atr_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // TestSeriesATRNoData tests no data scenario 11 | // 12 | // t=time.Time (no iteration) | | 13 | // p=ValueSeries | | 14 | // atr=ValueSeries | | 15 | func TestSeriesATRNoData(t *testing.T) { 16 | 17 | start := time.Now() 18 | data := OHLCVTestData(start, 4, 5*60*1000) 19 | 20 | series, err := NewOHLCVSeries(data) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | prop := OHLCVAttr(series, OHLCPropClose) 26 | atr := ATR(prop, 2) 27 | if atr == nil { 28 | t.Error("Expected to be non nil but got nil") 29 | } 30 | } 31 | 32 | // TestSeriesATRNoIteration tests this sceneario where there's no iteration yet 33 | // 34 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 | 35 | // p=ValueSeries | 14 | 15 | 17 | 18 | 36 | // atr=ValueSeries | | | | | 37 | func TestSeriesATRNoIteration(t *testing.T) { 38 | 39 | start := time.Now() 40 | data := OHLCVTestData(start, 4, 5*60*1000) 41 | data[0].C = 14 42 | data[1].C = 15 43 | data[2].C = 17 44 | data[3].C = 18 45 | 46 | series, err := NewOHLCVSeries(data) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | prop := OHLCVAttr(series, OHLCPropClose) 52 | atr := ATR(prop, 2) 53 | if atr == nil { 54 | t.Error("Expected to be non-nil but got nil") 55 | } 56 | } 57 | 58 | // TestSeriesATRIteration tests this scneario 59 | // 60 | // t=time.Time | 1 | 2 | 3 | 4 61 | // close=ValueSeries | 13 | 15 | 11 | 19 62 | // high=ValueSeries | 13.2 | 16 | 11.2 | 19 63 | // low=ValueSeries | 12.6 | 14.8 | 11 | 19 64 | // TR=ValueSeries | 0.6 | 3 | 4 | 8 65 | // ATR=ValueSeries | nil | nil | 2.5333| 4.3556 66 | func TestSeriesATRIteration(t *testing.T) { 67 | 68 | start := time.Now() 69 | data := OHLCVTestData(start, 4, 5*60*1000) 70 | data[0].C = 13 71 | data[0].H = 13.2 72 | data[0].L = 12.6 73 | data[1].C = 15 74 | data[1].H = 16 75 | data[1].L = 14.8 76 | data[2].C = 11 77 | data[2].H = 11.2 78 | data[2].L = 11 79 | data[3].C = 19 80 | data[3].H = 19 81 | data[3].L = 19 82 | 83 | series, err := NewOHLCVSeries(data) 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | 88 | testTable := []float64{0, 0, 2.5333, 4.3556} 89 | 90 | for i, v := range testTable { 91 | series.Next() 92 | 93 | prop := OHLCVAttr(series, OHLCPropTRHL) 94 | atr := ATR(prop, 3) 95 | exp := v 96 | if exp == 0 { 97 | if atr.Val() != nil { 98 | t.Fatalf("expected nil but got non nil: %+v testtable item: %d", *atr.Val(), i) 99 | } 100 | // OK 101 | } 102 | if exp != 0 { 103 | if atr.Val() == nil { 104 | t.Fatalf("expected non nil: %+v but got nil testtable item: %d", exp, i) 105 | } 106 | if fmt.Sprintf("%.04f", exp) != fmt.Sprintf("%.04f", *atr.Val()) { 107 | t.Fatalf("expected %+v but got %+v testtable item: %d", exp, *atr.Val(), i) 108 | } 109 | // OK 110 | } 111 | } 112 | } 113 | 114 | // TestSeriesATRNotEnoughData tests when the lookback is more than the number of data available 115 | // 116 | // t=time.Time | 1 | 2 | 3 | 4 (here) | 117 | // p=ValueSeries | 14 | 15 | 17 | 18 | 118 | // atr(close, 5) | nil| nil | nil| nil | 119 | func TestSeriesATRNotEnoughData(t *testing.T) { 120 | 121 | start := time.Now() 122 | data := OHLCVTestData(start, 4, 5*60*1000) 123 | data[0].C = 13 124 | data[1].C = 15 125 | data[2].C = 11 126 | data[3].C = 18 127 | 128 | series, err := NewOHLCVSeries(data) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | 133 | series.Next() 134 | series.Next() 135 | series.Next() 136 | series.Next() 137 | 138 | testTable := []struct { 139 | lookback int 140 | exp *float64 141 | }{ 142 | { 143 | lookback: 5, 144 | exp: nil, 145 | }, 146 | { 147 | lookback: 6, 148 | exp: nil, 149 | }, 150 | } 151 | 152 | for i, v := range testTable { 153 | prop := OHLCVAttr(series, OHLCPropClose) 154 | 155 | atr := ATR(prop, int64(v.lookback)) 156 | if atr == nil { 157 | t.Errorf("Expected to be non nil but got nil at idx: %d", i) 158 | } 159 | if atr.Val() != v.exp { 160 | t.Errorf("Expected to get %+v but got %+v for lookback %+v", v.exp, *atr.Val(), v.lookback) 161 | } 162 | } 163 | } 164 | 165 | func TestMemoryLeakATR(t *testing.T) { 166 | testMemoryLeak(t, func(o OHLCVSeries) error { 167 | c := OHLCVAttr(o, OHLCPropClose) 168 | ATR(c, 7) 169 | return nil 170 | }) 171 | } 172 | 173 | func ExampleATR() { 174 | start := time.Now() 175 | data := OHLCVTestData(start, 10000, 5*60*1000) 176 | series, _ := NewOHLCVSeries(data) 177 | 178 | for { 179 | if v, _ := series.Next(); v == nil { 180 | break 181 | } 182 | tr := OHLCVAttr(series, OHLCPropTR) 183 | atr := ATR(tr, 3) 184 | if atr.Val() != nil { 185 | log.Printf("ATR value: %+v", *atr.Val()) 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /pine/series_cci.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // CCI generates a ValueSeries of exponential moving average. 8 | func CCI(tp ValueSeries, l int64) ValueSeries { 9 | key := fmt.Sprintf("cci:%s:%d", tp.ID(), l) 10 | cci := getCache(key) 11 | if cci == nil { 12 | cci = NewValueSeries() 13 | } 14 | 15 | tpv := tp.GetCurrent() 16 | if tpv == nil { 17 | return cci 18 | } 19 | 20 | ma := SMA(tp, l) 21 | 22 | // need moving average to perform this 23 | if ma.GetCurrent() == nil { 24 | return cci 25 | } 26 | mav := ma.GetCurrent().v 27 | mdv := SubConstNoCache(tp, mav) 28 | 29 | // get absolute value 30 | mdvabs := OperateNoCache(mdv, mdv, "cci:absval", func(a, b float64) float64 { 31 | if a < 0 { 32 | return -1 * a 33 | } 34 | return a 35 | }) 36 | mdvabssum := SumNoCache(mdvabs, int(l)) 37 | md := DivConstNoCache(mdvabssum, float64(l)) 38 | denom := MulConstNoCache(md, 0.015) 39 | cci = DivNoCache(mdv, denom) 40 | 41 | setCache(key, cci) 42 | 43 | cci.SetCurrent(tpv.t) 44 | 45 | return cci 46 | } 47 | -------------------------------------------------------------------------------- /pine/series_cci_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // TestSeriesCCI tests no data scenario 11 | // 12 | // t=time.Time (no iteration) | | 13 | // p=ValueSeries | | 14 | // cci=ValueSeries | | 15 | func TestSeriesCCI(t *testing.T) { 16 | 17 | data := OHLCVStaticTestData() 18 | 19 | series, err := NewOHLCVSeries(data) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | tp := OHLCVAttr(series, OHLCPropHLC3) 24 | 25 | cci := CCI(tp, 3) 26 | if cci == nil { 27 | t.Error("Expected cci to be non nil but got nil") 28 | } 29 | } 30 | 31 | // TestSeriesCCINoIteration tests this sceneario where there's no iteration yet 32 | 33 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 | 34 | // p=ValueSeries | 14 | 15 | 17 | 18 | 35 | // cci=ValueSeries | | | | | 36 | func TestSeriesCCINoIteration(t *testing.T) { 37 | 38 | data := OHLCVStaticTestData() 39 | 40 | series, err := NewOHLCVSeries(data) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | tp := OHLCVAttr(series, OHLCPropHLC3) 45 | 46 | cci := CCI(tp, 3) 47 | if cci == nil { 48 | t.Error("Expected cci to be non nil but got nil") 49 | } 50 | } 51 | 52 | // TestSeriesCCIIteration tests the output against TradingView's expected values 53 | func TestSeriesCCIIteration(t *testing.T) { 54 | data := OHLCVStaticTestData() 55 | series, err := NewOHLCVSeries(data) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | tests := []*float64{ 61 | nil, 62 | nil, 63 | nil, 64 | NewFloat64(-133.3), 65 | NewFloat64(65.3), 66 | NewFloat64(17.5), 67 | NewFloat64(-2.7), 68 | NewFloat64(-133.3), 69 | NewFloat64(22.2), 70 | NewFloat64(-98.6), 71 | } 72 | 73 | for i, v := range tests { 74 | series.Next() 75 | tp := OHLCVAttr(series, OHLCPropHLC3) 76 | cci := CCI(tp, 4) 77 | 78 | // cci line 79 | if (cci.Val() == nil) != (v == nil) { 80 | if cci.Val() != nil { 81 | t.Errorf("Expected cci to be nil: %t but got %+v for iteration: %d", v == nil, *cci.Val(), i) 82 | } else { 83 | t.Errorf("Expected cci to be: %+v but got %+v for iteration: %d", *v, cci.Val(), i) 84 | } 85 | continue 86 | } 87 | if v != nil && fmt.Sprintf("%.01f", *v) != fmt.Sprintf("%.01f", *cci.Val()) { 88 | t.Errorf("Expected cci to be %+v but got %+v for iteration: %d", *v, *cci.Val(), i) 89 | } 90 | } 91 | } 92 | 93 | func TestMemoryLeakCCI(t *testing.T) { 94 | testMemoryLeak(t, func(o OHLCVSeries) error { 95 | c := OHLCVAttr(o, OHLCPropClose) 96 | CCI(c, 7) 97 | return nil 98 | }) 99 | } 100 | 101 | func BenchmarkCCI(b *testing.B) { 102 | // run the Fib function b.N times 103 | start := time.Now() 104 | data := OHLCVTestData(start, 10000, 5*60*1000) 105 | series, _ := NewOHLCVSeries(data) 106 | 107 | for n := 0; n < b.N; n++ { 108 | series.Next() 109 | tp := OHLCVAttr(series, OHLCPropHLC3) 110 | CCI(tp, 12) 111 | } 112 | } 113 | 114 | func ExampleCCI() { 115 | start := time.Now() 116 | data := OHLCVTestData(start, 10000, 5*60*1000) 117 | series, _ := NewOHLCVSeries(data) 118 | tp := OHLCVAttr(series, OHLCPropHLC3) 119 | cci := CCI(tp, 12) 120 | log.Printf("CCI line: %+v", cci.Val()) 121 | } 122 | -------------------------------------------------------------------------------- /pine/series_change.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Change compares the current `source` value to its value `lookback` bars ago and returns the difference. 8 | // 9 | // arguments are 10 | // - src: ValueSeries - Source data to seek difference 11 | // - lookback: int - Lookback to compare the change 12 | func Change(src ValueSeries, lookback int) ValueSeries { 13 | key := fmt.Sprintf("change:%s:%s:%d", src.ID(), src.ID(), lookback) 14 | chg := getCache(key) 15 | if chg == nil { 16 | chg = NewValueSeries() 17 | } 18 | 19 | // current available value 20 | stop := src.GetCurrent() 21 | 22 | if stop == nil { 23 | return chg 24 | } 25 | 26 | chg = change(*stop, src, chg, lookback) 27 | 28 | setCache(key, chg) 29 | 30 | chg.SetCurrent(stop.t) 31 | 32 | return chg 33 | } 34 | 35 | func change(stop Value, src, chg ValueSeries, l int) ValueSeries { 36 | 37 | var val *Value 38 | 39 | lastvw := chg.GetCurrent() 40 | if lastvw != nil { 41 | val = src.Get(lastvw.t) 42 | if val != nil { 43 | val = val.next 44 | } 45 | } else { 46 | val = src.GetFirst() 47 | } 48 | 49 | if val == nil { 50 | return chg 51 | } 52 | 53 | // populate src values 54 | condSrc := make([]float64, 0) 55 | 56 | prevVal := val 57 | for { 58 | prevVal = prevVal.prev 59 | if prevVal == nil { 60 | break 61 | } 62 | 63 | b := src.Get(prevVal.t) 64 | if b == nil { 65 | continue 66 | } 67 | 68 | srcv := src.Get(prevVal.t) 69 | // add at the beginning since we go backwards 70 | condSrc = append([]float64{srcv.v}, condSrc...) 71 | 72 | if len(condSrc) == (l + 1) { 73 | break 74 | } 75 | } 76 | 77 | // last available does not exist. start from first 78 | 79 | for { 80 | if val == nil { 81 | break 82 | } 83 | // update 84 | 85 | srcval := src.Get(val.t) 86 | if srcval != nil { 87 | condSrc = append(condSrc, srcval.v) 88 | if len(condSrc) > (l + 1) { 89 | condSrc = condSrc[1:] 90 | } 91 | } 92 | 93 | if len(condSrc) == (l + 1) { 94 | vwappend := condSrc[0] 95 | chg.Set(val.t, val.v-vwappend) 96 | } 97 | 98 | val = val.next 99 | } 100 | 101 | return chg 102 | } 103 | 104 | func NewFloat64(v float64) *float64 { 105 | v2 := v 106 | return &v2 107 | } 108 | -------------------------------------------------------------------------------- /pine/series_change_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // TestSeriesChangeNoData tests no data scenario 10 | // 11 | // t=time.Time (no iteration) | | 12 | // p=ValueSeries | | 13 | // change=ValueSeries | | 14 | func TestSeriesChangeNoData(t *testing.T) { 15 | 16 | start := time.Now() 17 | data := OHLCVTestData(start, 0, 5*60*1000) 18 | 19 | series, err := NewOHLCVSeries(data) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | src := OHLCVAttr(series, OHLCPropClose) 25 | 26 | rsi := Change(src, 2) 27 | if rsi == nil { 28 | t.Error("Expected to be non nil but got nil") 29 | } 30 | } 31 | 32 | // TestSeriesChangeNoIteration tests this sceneario where there's no iteration yet 33 | // 34 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 35 | // src=ValueSeries | 11 | 14 | 12 | 13 36 | // change(src, 1) | nil | 3 | -2 | 1 37 | // change(src, 2) | nil | nil | 1 | -1 38 | // change(src, 3) | nil | nil | nil | 2 39 | func TestSeriesChangeNoIteration(t *testing.T) { 40 | 41 | start := time.Now() 42 | data := OHLCVTestData(start, 4, 5*60*1000) 43 | data[0].C = 11 44 | data[1].C = 14 45 | data[2].C = 12 46 | data[3].C = 13 47 | 48 | series, err := NewOHLCVSeries(data) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | src := OHLCVAttr(series, OHLCPropClose) 54 | rsi := Change(src, 1) 55 | if rsi == nil { 56 | t.Error("Expected to be non-nil but got nil") 57 | } 58 | } 59 | 60 | // TestSeriesChangeSuccess tests this scneario when the iterator is at t=4 is not at the end 61 | // 62 | // t=time.Time | 1 | 2 | 3 | 4 63 | // src=ValueSeries | 11 | 14 | 12 | 13 64 | // change(src, 1) | nil | 3 | -2 | 1 65 | // change(src, 2) | nil | nil | 1 | -1 66 | // change(src, 3) | nil | nil | nil | 2 67 | func TestSeriesChangeSuccess(t *testing.T) { 68 | 69 | start := time.Now() 70 | data := OHLCVTestData(start, 4, 5*60*1000) 71 | data[0].C = 11 72 | data[1].C = 14 73 | data[2].C = 12 74 | data[3].C = 13 75 | 76 | series, err := NewOHLCVSeries(data) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | 81 | testTable := []struct { 82 | lookback int 83 | vals []float64 84 | }{ 85 | { 86 | lookback: 1, 87 | vals: []float64{0, 3, -2, 1}, 88 | }, 89 | { 90 | lookback: 2, 91 | vals: []float64{0, 0, 1, -1}, 92 | }, 93 | { 94 | lookback: 3, 95 | vals: []float64{0, 0, 0, 2}, 96 | }, 97 | } 98 | 99 | for j := 0; j <= 3; j++ { 100 | series.Next() 101 | 102 | for i, v := range testTable { 103 | src := OHLCVAttr(series, OHLCPropClose) 104 | vw := Change(src, v.lookback) 105 | exp := v.vals[j] 106 | if exp == 0 { 107 | if vw.Val() != nil { 108 | t.Fatalf("expected nil but got non nil: %+v at vals item: %d, testtable item: %d", *vw.Val(), j, i) 109 | } 110 | // OK 111 | } 112 | if exp != 0 { 113 | if vw.Val() == nil { 114 | t.Fatalf("expected non nil: %+v but got nil at vals item: %d, testtable item: %d", exp, j, i) 115 | } 116 | if exp != *vw.Val() { 117 | t.Fatalf("expected %+v but got %+v at vals item: %d, testtable item: %d", exp, *vw.Val(), j, i) 118 | } 119 | // OK 120 | } 121 | } 122 | } 123 | } 124 | 125 | // TestSeriesChangeNotEnoughData tests this scneario when the lookback is more than the number of data available 126 | // 127 | // t=time.Time | 1 | 2 | 3 | 4 128 | // src=ValueSeries | 11 | 14 | 12 | 13 129 | // change(src, 1) | nil | 3 | -2 | 1 130 | // change(src, 2) | nil | nil | 1 | -1 131 | // change(src, 3) | nil | nil | nil | 2 132 | func TestSeriesChangeNotEnoughData(t *testing.T) { 133 | 134 | start := time.Now() 135 | data := OHLCVTestData(start, 4, 5*60*1000) 136 | data[0].C = 13 137 | data[1].C = 15 138 | data[2].C = 11 139 | data[3].C = 18 140 | 141 | series, err := NewOHLCVSeries(data) 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | 146 | series.Next() 147 | series.Next() 148 | series.Next() 149 | series.Next() 150 | 151 | src := OHLCVAttr(series, OHLCPropClose) 152 | 153 | vw := Change(src, 4) 154 | if vw.Val() != nil { 155 | t.Errorf("Expected nil but got %+v", *vw.Val()) 156 | } 157 | } 158 | 159 | func TestMemoryLeakChange(t *testing.T) { 160 | testMemoryLeak(t, func(o OHLCVSeries) error { 161 | c := OHLCVAttr(o, OHLCPropClose) 162 | Change(c, 7) 163 | return nil 164 | }) 165 | } 166 | 167 | func BenchmarkChange(b *testing.B) { 168 | // run the Fib function b.N times 169 | start := time.Now() 170 | data := OHLCVTestData(start, 10000, 5*60*1000) 171 | series, _ := NewOHLCVSeries(data) 172 | vals := OHLCVAttr(series, OHLCPropClose) 173 | 174 | for n := 0; n < b.N; n++ { 175 | series.Next() 176 | Change(vals, 5) 177 | } 178 | } 179 | 180 | func ExampleChange() { 181 | start := time.Now() 182 | data := OHLCVTestData(start, 10000, 5*60*1000) 183 | series, _ := NewOHLCVSeries(data) 184 | for { 185 | if v, _ := series.Next(); v == nil { 186 | break 187 | } 188 | 189 | close := OHLCVAttr(series, OHLCPropClose) 190 | chg := Change(close, 12) 191 | log.Printf("Change line: %+v", chg.Val()) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /pine/series_cross.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | // Cross generates ValueSeries of ketler channel's middle, upper and lower in that order. 4 | func Cross(a, b ValueSeries) ValueSeries { 5 | c := OperateWithNil(a, b, "cross", func(av, bv *Value) *Value { 6 | if av == nil || bv == nil { 7 | return nil 8 | } 9 | zero := &Value{ 10 | t: av.t, 11 | v: 0, 12 | } 13 | if av.prev == nil || bv.prev == nil { 14 | return zero 15 | } 16 | if av.v < bv.v && av.prev.v > bv.prev.v || 17 | av.v > bv.v && av.prev.v < bv.prev.v { 18 | return &Value{ 19 | t: av.t, 20 | v: 1.0, 21 | } 22 | } 23 | return zero 24 | }) 25 | 26 | return c 27 | } 28 | -------------------------------------------------------------------------------- /pine/series_cross_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // TestSeriesCross tests no data scenario 10 | func TestSeriesCross(t *testing.T) { 11 | 12 | data := OHLCVStaticTestData() 13 | 14 | series, err := NewOHLCVSeries(data) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | c := OHLCVAttr(series, OHLCPropClose) 20 | o := OHLCVAttr(series, OHLCPropOpen) 21 | co := Cross(c, o) 22 | if co == nil { 23 | t.Error("Expected co to be non nil but got nil") 24 | } 25 | } 26 | 27 | // TestSeriesCrossNoIteration tests this sceneario where there's no iteration yet 28 | func TestSeriesCrossNoIteration(t *testing.T) { 29 | 30 | data := OHLCVStaticTestData() 31 | series, err := NewOHLCVSeries(data) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | c := OHLCVAttr(series, OHLCPropClose) 37 | o := OHLCVAttr(series, OHLCPropOpen) 38 | co := Cross(c, o) 39 | if co == nil { 40 | t.Error("Expected co to be non nil but got nil") 41 | } 42 | } 43 | 44 | // TestSeriesCrossIteration tests the output against TradingView's expected values 45 | func TestSeriesCrossIteration(t *testing.T) { 46 | data := OHLCVStaticTestData() 47 | series, err := NewOHLCVSeries(data) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | // array in order of Middle, Upper, Lower 52 | tests := []float64{ 53 | 0.0, 54 | 0.0, 55 | 0.0, 56 | 1.0, 57 | 1.0, 58 | 1.0, 59 | 0.0, 60 | 1.0, 61 | 1.0, 62 | 0.0, 63 | } 64 | 65 | for i, v := range tests { 66 | series.Next() 67 | 68 | c := OHLCVAttr(series, OHLCPropClose) 69 | o := OHLCVAttr(series, OHLCPropOpen) 70 | co := Cross(c, o) 71 | 72 | // Lower line 73 | if *co.Val() != v { 74 | t.Errorf("Expected lower to be %+v but got %+v for iteration: %d", v, *co.Val(), i) 75 | } 76 | } 77 | } 78 | 79 | func TestMemoryLeakCross(t *testing.T) { 80 | testMemoryLeak(t, func(o OHLCVSeries) error { 81 | c := OHLCVAttr(o, OHLCPropClose) 82 | op := OHLCVAttr(o, OHLCPropOpen) 83 | Cross(c, op) 84 | return nil 85 | }) 86 | } 87 | 88 | func BenchmarkCross(b *testing.B) { 89 | // run the Fib function b.N times 90 | start := time.Now() 91 | data := OHLCVTestData(start, 10000, 5*60*1000) 92 | series, _ := NewOHLCVSeries(data) 93 | 94 | for n := 0; n < b.N; n++ { 95 | series.Next() 96 | c := OHLCVAttr(series, OHLCPropClose) 97 | o := OHLCVAttr(series, OHLCPropOpen) 98 | Cross(c, o) 99 | } 100 | } 101 | 102 | func ExampleCross() { 103 | start := time.Now() 104 | data := OHLCVTestData(start, 10000, 5*60*1000) 105 | series, _ := NewOHLCVSeries(data) 106 | c := OHLCVAttr(series, OHLCPropClose) 107 | o := OHLCVAttr(series, OHLCPropOpen) 108 | co := Cross(c, o) 109 | log.Printf("Did Cross? = %t", *co.Val() == 1.0) 110 | } 111 | -------------------------------------------------------------------------------- /pine/series_crossover.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | // Crossover generates ValueSeries of ketler channel's middle, upper and lower in that order. 4 | func Crossover(a, b ValueSeries) ValueSeries { 5 | c := OperateWithNil(a, b, "crossover", func(av, bv *Value) *Value { 6 | if av == nil || bv == nil { 7 | return nil 8 | } 9 | zero := &Value{ 10 | t: av.t, 11 | v: 0, 12 | } 13 | if av.prev == nil || bv.prev == nil { 14 | return zero 15 | } 16 | if av.v > bv.v && av.prev.v < bv.prev.v { 17 | return &Value{ 18 | t: av.t, 19 | v: 1.0, 20 | } 21 | } 22 | return zero 23 | }) 24 | 25 | return c 26 | } 27 | -------------------------------------------------------------------------------- /pine/series_crossover_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // TestSeriesCrossover tests no data scenario 12 | func TestSeriesCrossover(t *testing.T) { 13 | 14 | data := OHLCVStaticTestData() 15 | 16 | series, err := NewOHLCVSeries(data) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | c := OHLCVAttr(series, OHLCPropClose) 22 | o := OHLCVAttr(series, OHLCPropOpen) 23 | co := Crossover(c, o) 24 | if co == nil { 25 | t.Error("Expected co to be non nil but got nil") 26 | } 27 | } 28 | 29 | // TestSeriesCrossoverNoIteration tests this sceneario where there's no iteration yet 30 | func TestSeriesCrossoverNoIteration(t *testing.T) { 31 | 32 | data := OHLCVStaticTestData() 33 | series, err := NewOHLCVSeries(data) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | c := OHLCVAttr(series, OHLCPropClose) 39 | o := OHLCVAttr(series, OHLCPropOpen) 40 | co := Crossover(c, o) 41 | if co == nil { 42 | t.Error("Expected co to be non nil but got nil") 43 | } 44 | } 45 | 46 | // TestSeriesCrossoverIteration tests the output against TradingView's expected values 47 | func TestSeriesCrossoverIteration(t *testing.T) { 48 | data := OHLCVStaticTestData() 49 | series, err := NewOHLCVSeries(data) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | // array in order of Middle, Upper, Lower 54 | tests := []float64{ 55 | 0.0, 56 | 0.0, 57 | 0.0, 58 | 0.0, 59 | 1.0, 60 | 0.0, 61 | 0.0, 62 | 1.0, 63 | 0.0, 64 | 0.0, 65 | } 66 | 67 | for i, v := range tests { 68 | series.Next() 69 | 70 | c := OHLCVAttr(series, OHLCPropClose) 71 | o := OHLCVAttr(series, OHLCPropOpen) 72 | co := Crossover(c, o) 73 | if err != nil { 74 | t.Fatal(errors.Wrap(err, "error Crossover")) 75 | } 76 | 77 | // Lower line 78 | if *co.Val() != v { 79 | t.Errorf("Expected lower to be %+v but got %+v for iteration: %d", v, *co.Val(), i) 80 | } 81 | } 82 | } 83 | 84 | func TestMemoryLeakCrossover(t *testing.T) { 85 | testMemoryLeak(t, func(o OHLCVSeries) error { 86 | c := OHLCVAttr(o, OHLCPropClose) 87 | op := OHLCVAttr(o, OHLCPropOpen) 88 | Crossover(c, op) 89 | return nil 90 | }) 91 | } 92 | 93 | func BenchmarkCrossover(b *testing.B) { 94 | // run the Fib function b.N times 95 | start := time.Now() 96 | data := OHLCVTestData(start, 10000, 5*60*1000) 97 | series, _ := NewOHLCVSeries(data) 98 | 99 | for n := 0; n < b.N; n++ { 100 | series.Next() 101 | c := OHLCVAttr(series, OHLCPropClose) 102 | o := OHLCVAttr(series, OHLCPropOpen) 103 | Crossover(c, o) 104 | } 105 | } 106 | 107 | func ExampleCrossover() { 108 | start := time.Now() 109 | data := OHLCVTestData(start, 10000, 5*60*1000) 110 | series, _ := NewOHLCVSeries(data) 111 | c := OHLCVAttr(series, OHLCPropClose) 112 | o := OHLCVAttr(series, OHLCPropOpen) 113 | co := Crossover(c, o) 114 | log.Printf("Did Crossover? = %t", *co.Val() == 1.0) 115 | } 116 | -------------------------------------------------------------------------------- /pine/series_crossunder.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | // Crossunder generates ValueSeries of ketler channel's middle, upper and lower in that order. 4 | func Crossunder(a, b ValueSeries) ValueSeries { 5 | c := OperateWithNil(a, b, "crossunder", func(av, bv *Value) *Value { 6 | if av == nil || bv == nil { 7 | return nil 8 | } 9 | zero := &Value{ 10 | t: av.t, 11 | v: 0, 12 | } 13 | if av.prev == nil || bv.prev == nil { 14 | return zero 15 | } 16 | if av.v < bv.v && av.prev.v > bv.prev.v { 17 | return &Value{ 18 | t: av.t, 19 | v: 1.0, 20 | } 21 | } 22 | return zero 23 | }) 24 | 25 | return c 26 | } 27 | -------------------------------------------------------------------------------- /pine/series_crossunder_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // TestSeriesCrossunder tests no data scenario 12 | func TestSeriesCrossunder(t *testing.T) { 13 | 14 | data := OHLCVStaticTestData() 15 | 16 | series, err := NewOHLCVSeries(data) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | c := OHLCVAttr(series, OHLCPropClose) 22 | o := OHLCVAttr(series, OHLCPropOpen) 23 | co := Crossunder(c, o) 24 | if co == nil { 25 | t.Error("Expected co to be non nil but got nil") 26 | } 27 | } 28 | 29 | // TestSeriesCrossunderNoIteration tests this sceneario where there's no iteration yet 30 | func TestSeriesCrossunderNoIteration(t *testing.T) { 31 | 32 | data := OHLCVStaticTestData() 33 | series, err := NewOHLCVSeries(data) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | c := OHLCVAttr(series, OHLCPropClose) 39 | o := OHLCVAttr(series, OHLCPropOpen) 40 | co := Crossunder(c, o) 41 | if co == nil { 42 | t.Error("Expected co to be non nil but got nil") 43 | } 44 | } 45 | 46 | // TestSeriesCrossunderIteration tests the output against TradingView's expected values 47 | func TestSeriesCrossunderIteration(t *testing.T) { 48 | data := OHLCVStaticTestData() 49 | series, err := NewOHLCVSeries(data) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | // array in order of Middle, Upper, Lower 54 | tests := []float64{ 55 | 0.0, 56 | 0.0, 57 | 0.0, 58 | 1.0, 59 | 0.0, 60 | 1.0, 61 | 0.0, 62 | 0.0, 63 | 1.0, 64 | 0.0, 65 | } 66 | 67 | for i, v := range tests { 68 | series.Next() 69 | 70 | c := OHLCVAttr(series, OHLCPropClose) 71 | o := OHLCVAttr(series, OHLCPropOpen) 72 | co := Crossunder(c, o) 73 | if err != nil { 74 | t.Fatal(errors.Wrap(err, "error Crossunder")) 75 | } 76 | 77 | // Lower line 78 | if *co.Val() != v { 79 | t.Errorf("Expected lower to be %+v but got %+v for iteration: %d", v, *co.Val(), i) 80 | } 81 | } 82 | } 83 | 84 | func TestMemoryLeakCrossunder(t *testing.T) { 85 | testMemoryLeak(t, func(o OHLCVSeries) error { 86 | c := OHLCVAttr(o, OHLCPropClose) 87 | op := OHLCVAttr(o, OHLCPropOpen) 88 | Crossunder(c, op) 89 | return nil 90 | }) 91 | } 92 | 93 | func BenchmarkCrossunder(b *testing.B) { 94 | // run the Fib function b.N times 95 | start := time.Now() 96 | data := OHLCVTestData(start, 10000, 5*60*1000) 97 | series, _ := NewOHLCVSeries(data) 98 | 99 | for n := 0; n < b.N; n++ { 100 | series.Next() 101 | c := OHLCVAttr(series, OHLCPropClose) 102 | o := OHLCVAttr(series, OHLCPropOpen) 103 | Crossunder(c, o) 104 | } 105 | } 106 | 107 | func ExampleCrossunder() { 108 | start := time.Now() 109 | data := OHLCVTestData(start, 10000, 5*60*1000) 110 | series, _ := NewOHLCVSeries(data) 111 | c := OHLCVAttr(series, OHLCPropClose) 112 | o := OHLCVAttr(series, OHLCPropOpen) 113 | co := Crossunder(c, o) 114 | log.Printf("Did Crossunder? = %t", *co.Val() == 1.0) 115 | } 116 | -------------------------------------------------------------------------------- /pine/series_diff_abs.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import "math" 4 | 5 | func DiffAbs(a, b ValueSeries) ValueSeries { 6 | return Operate(a, b, "diffabs", func(av, bv float64) float64 { 7 | d := av - bv 8 | return math.Abs(d) 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /pine/series_dmi.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // DMI generates a ValueSeries of directional movement index. 8 | func DMI(ohlcv OHLCVSeries, len, smoo int) (adx, plus, minus ValueSeries) { 9 | adxkey := fmt.Sprintf("adx:%s:%d:%d", ohlcv.ID(), len, smoo) 10 | adx = getCache(adxkey) 11 | if adx == nil { 12 | adx = NewValueSeries() 13 | } 14 | 15 | pluskey := fmt.Sprintf("plus:%s:%d:%d", ohlcv.ID(), len, smoo) 16 | plus = getCache(pluskey) 17 | if plus == nil { 18 | plus = NewValueSeries() 19 | } 20 | 21 | minuskey := fmt.Sprintf("minus:%s:%d:%d", ohlcv.ID(), len, smoo) 22 | minus = getCache(minuskey) 23 | if minus == nil { 24 | minus = NewValueSeries() 25 | } 26 | 27 | h := OHLCVAttr(ohlcv, OHLCPropHigh) 28 | stop := h.GetCurrent() 29 | if stop == nil { 30 | return 31 | } 32 | 33 | l := OHLCVAttr(ohlcv, OHLCPropLow) 34 | tr := OHLCVAttr(ohlcv, OHLCPropTRHL) 35 | 36 | up := Change(h, 1) 37 | down := Change(l, 1) 38 | plusdm := Operate(up, down, "dmi:uv", func(uv, dv float64) float64 { 39 | dv = dv * -1 40 | if uv > dv && uv > 0 { 41 | return uv 42 | } 43 | return 0 44 | }) 45 | minusdm := Operate(down, up, "dmi:uv", func(dv, uv float64) float64 { 46 | dv = dv * -1 47 | if dv > uv && dv > 0 { 48 | return dv 49 | } 50 | return 0 51 | }) 52 | trurange := RMA(tr, int64(len)) 53 | plusdmrma := RMA(plusdm, int64(len)) 54 | minusdmrma := RMA(minusdm, int64(len)) 55 | plus = MulConst(Div(plusdmrma, trurange), 100) 56 | minus = MulConst(Div(minusdmrma, trurange), 100) 57 | 58 | sum := Add(plus, minus) 59 | denom := Operate(sum, sum, "dmi:denom", func(a, b float64) float64 { 60 | if a == 0 { 61 | return 1 62 | } 63 | return a 64 | }) 65 | 66 | adxrma := RMA(Div(DiffAbs(plus, minus), denom), 3) 67 | adx = MulConst(adxrma, 100) 68 | 69 | setCache(adxkey, adx) 70 | setCache(pluskey, plus) 71 | setCache(minuskey, minus) 72 | 73 | return adx, plus, minus 74 | } 75 | -------------------------------------------------------------------------------- /pine/series_dmi_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // TestSeriesDMI tests no data scenario 11 | // 12 | // t=time.Time (no iteration) | | 13 | // p=ValueSeries | | 14 | // dmi=ValueSeries | | 15 | func TestSeriesDMI(t *testing.T) { 16 | 17 | start := time.Now() 18 | data := OHLCVTestData(start, 4, 5*60*1000) 19 | 20 | series, err := NewOHLCVSeries(data) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | adx, dip, dim := DMI(series, 15, 3) 26 | if dip == nil { 27 | t.Error("Expected dip to be non nil but got nil") 28 | } 29 | if dim == nil { 30 | t.Error("Expected dim to be non nil but got nil") 31 | } 32 | if adx == nil { 33 | t.Error("Expected adx to be non nil but got nil") 34 | } 35 | } 36 | 37 | // TestSeriesDMINoIteration tests this sceneario where there's no iteration yet 38 | func TestSeriesDMINoIteration(t *testing.T) { 39 | 40 | start := time.Now() 41 | data := OHLCVTestData(start, 4, 5*60*1000) 42 | data[0].C = 14 43 | data[1].C = 15 44 | data[2].C = 17 45 | data[3].C = 18 46 | 47 | series, err := NewOHLCVSeries(data) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | adx, _, _ := DMI(series, 3, 2) 53 | if adx == nil { 54 | t.Error("Expected dmi to be non nil but got nil") 55 | } 56 | } 57 | 58 | // TestSeriesDMIIteration tests the output against TradingView's expected values 59 | func TestSeriesDMIIteration(t *testing.T) { 60 | data := OHLCVStaticTestData() 61 | series, err := NewOHLCVSeries(data) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | // array in order of ADX, DI+, DI- 67 | tests := [][]*float64{ 68 | nil, 69 | nil, 70 | nil, 71 | nil, 72 | {nil, NewFloat64(2.49), NewFloat64(7.78)}, 73 | {nil, NewFloat64(2.96), NewFloat64(6.17)}, 74 | {NewFloat64(45.43), NewFloat64(2.3), NewFloat64(6.82)}, 75 | {NewFloat64(56.3), NewFloat64(1.6), NewFloat64(12.97)}, 76 | {NewFloat64(63.55), NewFloat64(1.2), NewFloat64(9.7)}, 77 | {NewFloat64(71.9), NewFloat64(0.9067), NewFloat64(14.99)}, 78 | } 79 | 80 | for i, v := range tests { 81 | series.Next() 82 | adx, dmip, dmim := DMI(series, 4, 2) 83 | 84 | // list can be empty 85 | if v == nil { 86 | // if adx.Val() != nil || 87 | if dmip.Val() != nil || dmim.Val() != nil { 88 | t.Errorf("Expected no values to be returned but got some at %d", i) 89 | } 90 | continue 91 | } 92 | 93 | // ADX line 94 | if v[0] != nil && fmt.Sprintf("%.01f", *v[0]) != fmt.Sprintf("%.01f", *adx.Val()) { 95 | t.Errorf("Expected dmi to be %+v but got %+v for iteration: %d", *v[0], *adx.Val(), i) 96 | } 97 | 98 | // DMI+ line 99 | if v != nil && fmt.Sprintf("%.01f", *v[1]) != fmt.Sprintf("%.01f", *dmip.Val()) { 100 | t.Errorf("Expected dmi to be %+v but got %+v for iteration: %d", *v[1], *dmip.Val(), i) 101 | } 102 | 103 | // DMI- line 104 | if v != nil && fmt.Sprintf("%.01f", *v[2]) != fmt.Sprintf("%.01f", *dmim.Val()) { 105 | t.Errorf("Expected dmi to be %+v but got %+v for iteration: %d", *v[2], *dmim.Val(), i) 106 | } 107 | } 108 | } 109 | 110 | func TestMemoryLeakDMI(t *testing.T) { 111 | testMemoryLeak(t, func(o OHLCVSeries) error { 112 | DMI(o, 4, 3) 113 | return nil 114 | }) 115 | } 116 | 117 | func BenchmarkDMI(b *testing.B) { 118 | // run the Fib function b.N times 119 | start := time.Now() 120 | data := OHLCVTestData(start, 10000, 5*60*1000) 121 | series, _ := NewOHLCVSeries(data) 122 | 123 | for n := 0; n < b.N; n++ { 124 | series.Next() 125 | DMI(series, 4, 3) 126 | } 127 | } 128 | 129 | func ExampleDMI() { 130 | start := time.Now() 131 | data := OHLCVTestData(start, 10000, 5*60*1000) 132 | series, _ := NewOHLCVSeries(data) 133 | adx, dmip, dmim := DMI(series, 4, 3) 134 | log.Printf("ADX: %+v, DI+: %+v, DI-: %+v", adx.Val(), dmip.Val(), dmim.Val()) 135 | } 136 | -------------------------------------------------------------------------------- /pine/series_ema.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // EMA generates a ValueSeries of exponential moving average. 8 | func EMA(p ValueSeries, l int64) ValueSeries { 9 | key := fmt.Sprintf("ema:%s:%d", p.ID(), l) 10 | ema := getCache(key) 11 | if ema == nil { 12 | ema = NewValueSeries() 13 | } 14 | 15 | if p == nil || p.GetCurrent() == nil { 16 | return ema 17 | } 18 | 19 | // current available value 20 | stop := p.GetCurrent() 21 | 22 | ema = getEMA(stop, p, ema, l) 23 | 24 | setCache(key, ema) 25 | 26 | ema.SetCurrent(stop.t) 27 | 28 | return ema 29 | } 30 | 31 | func getEMA(stop *Value, vs ValueSeries, ema ValueSeries, l int64) ValueSeries { 32 | 33 | var mul float64 = 2.0 / float64(l+1.0) 34 | firstVal := ema.GetLast() 35 | 36 | if firstVal == nil { 37 | firstVal = vs.GetFirst() 38 | } 39 | 40 | if firstVal == nil { 41 | // if nothing is available, then nothing can be done 42 | return ema 43 | } 44 | 45 | itervt := firstVal.t 46 | 47 | var fseek int64 48 | var ftot float64 49 | 50 | for { 51 | v := vs.Get(itervt) 52 | if v == nil { 53 | break 54 | } 55 | e := ema.Get(itervt) 56 | if e != nil && v.next == nil { 57 | break 58 | } 59 | if e != nil { 60 | itervt = v.next.t 61 | continue 62 | } 63 | 64 | // get previous ema 65 | if v.prev != nil { 66 | prevv := vs.Get(v.prev.t) 67 | preve := ema.Get(prevv.t) 68 | // previous ema exists, just do multiplication to that 69 | if preve != nil { 70 | nextEMA := (v.v-preve.v)*mul + preve.v 71 | ema.Set(v.t, nextEMA) 72 | continue 73 | } 74 | } 75 | 76 | // previous value does not exist. just keep adding until multplication is required 77 | fseek++ 78 | ftot = ftot + v.v 79 | 80 | if fseek == l { 81 | avg := ftot / float64(fseek) 82 | ema.Set(v.t, avg) 83 | } 84 | 85 | if v.next == nil { 86 | break 87 | } 88 | if v.t.Equal(stop.t) { 89 | break 90 | } 91 | itervt = v.next.t 92 | } 93 | 94 | return ema 95 | } 96 | -------------------------------------------------------------------------------- /pine/series_ema_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // TestSeriesEMANoData tests no data scenario 10 | // 11 | // t=time.Time (no iteration) | | 12 | // p=ValueSeries | | 13 | // ema=ValueSeries | | 14 | func TestSeriesEMANoData(t *testing.T) { 15 | 16 | start := time.Now() 17 | data := OHLCVTestData(start, 4, 5*60*1000) 18 | 19 | series, err := NewOHLCVSeries(data) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | prop := OHLCVAttr(series, OHLCPropClose) 25 | ema := EMA(prop, 2) 26 | if ema == nil { 27 | t.Error("Expected to be non nil but got nil") 28 | } 29 | } 30 | 31 | // TestSeriesEMANoIteration tests this sceneario where there's no iteration yet 32 | // 33 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 | 34 | // p=ValueSeries | 14 | 15 | 17 | 18 | 35 | // ema=ValueSeries | | | | | 36 | func TestSeriesEMANoIteration(t *testing.T) { 37 | 38 | start := time.Now() 39 | data := OHLCVTestData(start, 4, 5*60*1000) 40 | data[0].C = 14 41 | data[1].C = 15 42 | data[2].C = 17 43 | data[3].C = 18 44 | 45 | series, err := NewOHLCVSeries(data) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | prop := OHLCVAttr(series, OHLCPropClose) 51 | ema := EMA(prop, 2) 52 | if ema == nil { 53 | t.Error("Expected to be non-nil but got nil") 54 | } 55 | } 56 | 57 | // TestSeriesEMAIteration4 tests this scneario when the iterator is at t=4 is not at the end 58 | // 59 | // t=time.Time | 1 | 2 | 3 | 4 (time here) | 60 | // p=ValueSeries | 13 | 15 | 17 | 18 | 61 | // ema(close, 1) | 13 | 15 | 17 | 18 | 62 | // ema(close, 2) | nil | 14 | 16 | 17.3333 | 63 | // ema(close, 3) | nil | nil | 15 | 16.5 | 64 | // ema(close, 4) | nil | nil | nil | 15.75 | 65 | // ema(close, 5) | nil | nil | nil | nil | 66 | func TestSeriesEMAIteration4(t *testing.T) { 67 | 68 | start := time.Now() 69 | data := OHLCVTestData(start, 4, 5*60*1000) 70 | data[0].C = 13 71 | data[1].C = 15 72 | data[2].C = 17 73 | data[3].C = 18 74 | 75 | series, err := NewOHLCVSeries(data) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | series.Next() 81 | series.Next() 82 | series.Next() 83 | series.Next() 84 | 85 | testTable := []struct { 86 | lookback int 87 | exp float64 88 | isNil bool 89 | }{ 90 | { 91 | lookback: 1, 92 | exp: 18, 93 | }, 94 | { 95 | lookback: 2, 96 | exp: 17.333333333333332, 97 | }, 98 | { 99 | lookback: 3, 100 | exp: 16.5, 101 | }, 102 | { 103 | lookback: 4, 104 | exp: 15.75, 105 | }, 106 | { 107 | lookback: 5, 108 | exp: 0, 109 | isNil: true, 110 | }, 111 | } 112 | 113 | for i, v := range testTable { 114 | prop := OHLCVAttr(series, OHLCPropClose) 115 | ema := EMA(prop, int64(v.lookback)) 116 | 117 | if ema == nil { 118 | t.Errorf("Expected to be non nil but got nil at idx: %d", i) 119 | } 120 | if v.isNil && ema.Val() != nil { 121 | t.Error("expected to be nil but got non nil") 122 | } 123 | if !v.isNil && *ema.Val() != v.exp { 124 | t.Errorf("Expected to get %+v but got %+v for lookback %+v", v.exp, *ema.Val(), v.lookback) 125 | } 126 | } 127 | } 128 | 129 | // TestSeriesEMAIteration3 tests this scneario when the iterator is at t=4 is not at the end 130 | // 131 | // t=time.Time | 1 | 2 | 3 (time here) | 4 | 132 | // p=ValueSeries | 13 | 15 | 17 | 18 | 133 | // ema(close, 1) | 13 | 14 | 17 | 18 | 134 | // ema(close, 2) | nil | 14 | 16 | 17.3333 | 135 | // ema(close, 3) | nil | nil | 15 | 16.5 | 136 | // ema(close, 4) | nil | nil | nil | 15.75 | 137 | // ema(close, 5) | nil | nil | nil | nil | 138 | func TestSeriesEMAIteration3(t *testing.T) { 139 | 140 | start := time.Now() 141 | data := OHLCVTestData(start, 4, 5*60*1000) 142 | data[0].C = 13 143 | data[1].C = 15 144 | data[2].C = 17 145 | data[3].C = 18 146 | 147 | series, err := NewOHLCVSeries(data) 148 | if err != nil { 149 | t.Fatal(err) 150 | } 151 | 152 | series.Next() 153 | series.Next() 154 | series.Next() 155 | 156 | testTable := []struct { 157 | lookback int 158 | exp float64 159 | isNil bool 160 | }{ 161 | { 162 | lookback: 1, 163 | exp: 17, 164 | }, 165 | { 166 | lookback: 2, 167 | exp: 16, 168 | }, 169 | { 170 | lookback: 3, 171 | exp: 15, 172 | }, 173 | { 174 | lookback: 4, 175 | exp: 0, 176 | isNil: true, 177 | }, 178 | { 179 | lookback: 5, 180 | exp: 0, 181 | isNil: true, 182 | }, 183 | } 184 | 185 | for i, v := range testTable { 186 | prop := OHLCVAttr(series, OHLCPropClose) 187 | ema := EMA(prop, int64(v.lookback)) 188 | 189 | if ema == nil { 190 | t.Errorf("Expected to be non nil but got nil at idx: %d", i) 191 | } 192 | if v.isNil && ema.Val() != nil { 193 | t.Error("expected to be nil but got non nil") 194 | } 195 | if !v.isNil && *ema.Val() != v.exp { 196 | t.Errorf("Expected to get %+v but got %+v for lookback %+v", v.exp, *ema.Val(), v.lookback) 197 | } 198 | } 199 | } 200 | 201 | // TestSeriesEMANested tests nested EMA 202 | // 203 | 204 | // TestSeriesEMAIteration3 tests this scneario when the iterator is at t=4 is not at the end 205 | // 206 | // t=time.Time | 1 | 2 | 3 | 4 (time here) | 207 | // p=ValueSeries | 13 | 15 | 17 | 18 | 208 | // ema(close, 1) | 13 | 14 | 17 | 18 | 209 | // ema(close, 2) | nil | 14 | 16 | 17.3333 | 210 | // ema(ema(close, 2), 2) | nil | nil | 15 | 16.5555 | 211 | func TestSeriesEMANested(t *testing.T) { 212 | 213 | start := time.Now() 214 | data := OHLCVTestData(start, 4, 5*60*1000) 215 | data[0].C = 13 216 | data[1].C = 15 217 | data[2].C = 17 218 | data[3].C = 18 219 | 220 | series, err := NewOHLCVSeries(data) 221 | if err != nil { 222 | t.Fatal(err) 223 | } 224 | 225 | series.Next() 226 | series.Next() 227 | series.Next() 228 | 229 | testTable := []float64{15, 16.555555555555554} 230 | 231 | for _, v := range testTable { 232 | prop := OHLCVAttr(series, OHLCPropClose) 233 | 234 | ema := EMA(prop, 2) 235 | ema2 := EMA(ema, 2) 236 | if *ema2.Val() != v { 237 | t.Errorf("expectd %+v but got %+v", v, *ema2.Val()) 238 | } 239 | series.Next() 240 | } 241 | } 242 | 243 | // TestSeriesEMANotEnoughData tests this scneario when the lookback is more than the number of data available 244 | // 245 | // t=time.Time | 1 | 2 | 3 | 4 (here) | 246 | // p=ValueSeries | 14 | 15 | 17 | 18 | 247 | // ema(close, 5) | nil| nil | nil| nil | 248 | func TestSeriesEMANotEnoughData(t *testing.T) { 249 | 250 | start := time.Now() 251 | data := OHLCVTestData(start, 4, 5*60*1000) 252 | data[0].C = 15 253 | data[1].C = 16 254 | data[2].C = 17 255 | data[3].C = 18 256 | 257 | log.Printf("Data[0].S, %+v, 3s: %+v", data[0].S, data[3].S) 258 | 259 | series, err := NewOHLCVSeries(data) 260 | if err != nil { 261 | t.Fatal(err) 262 | } 263 | 264 | series.Next() 265 | series.Next() 266 | series.Next() 267 | series.Next() 268 | 269 | testTable := []struct { 270 | lookback int 271 | exp *float64 272 | }{ 273 | { 274 | lookback: 5, 275 | exp: nil, 276 | }, 277 | { 278 | lookback: 6, 279 | exp: nil, 280 | }, 281 | } 282 | 283 | for i, v := range testTable { 284 | prop := OHLCVAttr(series, OHLCPropClose) 285 | 286 | ema := EMA(prop, int64(v.lookback)) 287 | if ema == nil { 288 | t.Errorf("Expected to be non nil but got nil at idx: %d", i) 289 | } 290 | if ema.Val() != v.exp { 291 | t.Errorf("Expected to get %+v but got %+v for lookback %+v", v.exp, ema, v.lookback) 292 | } 293 | } 294 | } 295 | 296 | func TestMemoryLeakEMA(t *testing.T) { 297 | testMemoryLeak(t, func(o OHLCVSeries) error { 298 | close := OHLCVAttr(o, OHLCPropClose) 299 | EMA(close, 20) 300 | return nil 301 | }) 302 | } 303 | 304 | func ExampleEMA() { 305 | start := time.Now() 306 | data := OHLCVTestData(start, 10000, 5*60*1000) 307 | series, _ := NewOHLCVSeries(data) 308 | for { 309 | if v, _ := series.Next(); v == nil { 310 | break 311 | } 312 | 313 | close := OHLCVAttr(series, OHLCPropClose) 314 | ema := EMA(close, 20) 315 | log.Printf("EMA: %+v", ema.Val()) 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /pine/series_kc.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | // KC generates ValueSeries of ketler channel's middle, upper and lower in that order. 4 | func KC(src ValueSeries, o OHLCVSeries, l int64, mult float64, usetr bool) (middle, upper, lower ValueSeries) { 5 | 6 | lower = NewValueSeries() 7 | upper = NewValueSeries() 8 | middle = NewValueSeries() 9 | start := src.GetCurrent() 10 | 11 | if start == nil { 12 | return middle, upper, lower 13 | } 14 | 15 | var span ValueSeries 16 | basis := EMA(src, l) 17 | 18 | if usetr { 19 | span = OHLCVAttr(o, OHLCPropTR) 20 | } else { 21 | h := OHLCVAttr(o, OHLCPropHigh) 22 | l := OHLCVAttr(o, OHLCPropLow) 23 | span = Sub(h, l) 24 | } 25 | 26 | rangeEma := EMA(span, l) 27 | 28 | middle = basis 29 | rangeEmaMul := MulConst(rangeEma, mult) 30 | upper = Add(basis, rangeEmaMul) 31 | lower = Sub(basis, rangeEmaMul) 32 | 33 | middle.SetCurrent(start.t) 34 | upper.SetCurrent(start.t) 35 | lower.SetCurrent(start.t) 36 | 37 | return middle, upper, lower 38 | } 39 | -------------------------------------------------------------------------------- /pine/series_kc_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // TestSeriesKC tests no data scenario 11 | func TestSeriesKC(t *testing.T) { 12 | 13 | data := OHLCVStaticTestData() 14 | 15 | series, err := NewOHLCVSeries(data) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | close := OHLCVAttr(series, OHLCPropClose) 20 | 21 | m, u, l := KC(close, series, 3, 2.5, true) 22 | if m == nil { 23 | t.Error("Expected kc to be non nil but got nil") 24 | } 25 | if u == nil { 26 | t.Error("Expected kc to be non nil but got nil") 27 | } 28 | if l == nil { 29 | t.Error("Expected kc to be non nil but got nil") 30 | } 31 | } 32 | 33 | // TestSeriesKCNoIteration tests this sceneario where there's no iteration yet 34 | func TestSeriesKCNoIteration(t *testing.T) { 35 | 36 | data := OHLCVStaticTestData() 37 | series, err := NewOHLCVSeries(data) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | close := OHLCVAttr(series, OHLCPropClose) 42 | 43 | m, u, l := KC(close, series, 3, 2.5, true) 44 | if m == nil { 45 | t.Error("Expected kc to be non nil but got nil") 46 | } 47 | if u == nil { 48 | t.Error("Expected kc to be non nil but got nil") 49 | } 50 | if l == nil { 51 | t.Error("Expected kc to be non nil but got nil") 52 | } 53 | } 54 | 55 | // TestSeriesKCIteration tests the output against TradingView's expected values 56 | func TestSeriesKCIteration(t *testing.T) { 57 | data := OHLCVStaticTestData() 58 | series, err := NewOHLCVSeries(data) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | // array in order of Middle, Upper, Lower 63 | tests := [][]*float64{ 64 | nil, 65 | nil, 66 | nil, 67 | {NewFloat64(16.33), NewFloat64(36.2), NewFloat64(-3.55)}, 68 | {NewFloat64(17.52), NewFloat64(37.74), NewFloat64(-2.71)}, 69 | {NewFloat64(16.19), NewFloat64(34.62), NewFloat64(-2.25)}, 70 | {NewFloat64(15.47), NewFloat64(33.13), NewFloat64(-2.19)}, 71 | {NewFloat64(13.68), NewFloat64(33.88), NewFloat64(-6.51)}, 72 | {NewFloat64(14.09), NewFloat64(32.81), NewFloat64(-4.63)}, 73 | {NewFloat64(12.57), NewFloat64(31.41), NewFloat64(-6.26)}, 74 | } 75 | 76 | for i, v := range tests { 77 | series.Next() 78 | c := OHLCVAttr(series, OHLCPropClose) 79 | m, u, l := KC(c, series, 4, 2.5, false) 80 | 81 | // list can be empty 82 | if v == nil { 83 | if m.Val() != nil || u.Val() != nil || l.Val() != nil { 84 | t.Errorf("Expected no values to be returned but got some at %d", i) 85 | } 86 | continue 87 | } 88 | 89 | // Middle line 90 | if v[0] != nil && fmt.Sprintf("%.01f", *v[0]) != fmt.Sprintf("%.01f", *m.Val()) { 91 | t.Errorf("Expected middle to be %+v but got %+v for iteration: %d", *v[0], *m.Val(), i) 92 | } 93 | 94 | // Upper line 95 | if v != nil && fmt.Sprintf("%.01f", *v[1]) != fmt.Sprintf("%.01f", *u.Val()) { 96 | t.Errorf("Expected upper to be %+v but got %+v for iteration: %d", *v[1], *u.Val(), i) 97 | } 98 | 99 | // Lower line 100 | if v != nil && fmt.Sprintf("%.01f", *v[2]) != fmt.Sprintf("%.01f", *l.Val()) { 101 | t.Errorf("Expected lower to be %+v but got %+v for iteration: %d", *v[2], *l.Val(), i) 102 | } 103 | } 104 | } 105 | 106 | func TestMemoryLeakKC(t *testing.T) { 107 | testMemoryLeak(t, func(o OHLCVSeries) error { 108 | KC(OHLCVAttr(o, OHLCPropClose), o, 4, 2.5, false) 109 | return nil 110 | }) 111 | } 112 | 113 | func BenchmarkKC(b *testing.B) { 114 | // run the Fib function b.N times 115 | start := time.Now() 116 | data := OHLCVTestData(start, 10000, 5*60*1000) 117 | series, _ := NewOHLCVSeries(data) 118 | 119 | for n := 0; n < b.N; n++ { 120 | series.Next() 121 | KC(OHLCVAttr(series, OHLCPropClose), series, 4, 2.5, false) 122 | } 123 | } 124 | 125 | func ExampleKC() { 126 | start := time.Now() 127 | data := OHLCVTestData(start, 10000, 5*60*1000) 128 | series, _ := NewOHLCVSeries(data) 129 | m, u, l := KC(OHLCVAttr(series, OHLCPropClose), series, 4, 2.5, false) 130 | log.Printf("KC middle line: %+v, upper: %+v, lower: %+v", m.Val(), u.Val(), l.Val()) 131 | } 132 | -------------------------------------------------------------------------------- /pine/series_macd.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // MACD generates a ValueSeries of MACD (moving average convergence/divergence). 8 | // It is supposed to reveal changes in the strength, direction, momentum, and duration of a trend in a stock's price. 9 | // 10 | // The formula for MACD is 11 | // 12 | // - MACD Line: (12-day EMA - 26-day EMA) 13 | // - Signal Line: 9-day EMA of MACD Line 14 | // - MACD Histogram: MACD Line - Signal Line 15 | // 16 | // The arguments are: 17 | // 18 | // - source: ValueSeries - source of data 19 | // - fastlen: int - fast len of MACD series 20 | // - slowlen: int - slow len of MACD series 21 | // - siglen: int - signal length of MACD series 22 | // 23 | // The return values are: 24 | // - macdLine: ValueSeries - MACD Line 25 | // - signalLine: ValueSeries - Signal Line 26 | // - histLine: ValueSeries - MACD Histogram 27 | // - err: error 28 | func MACD(src ValueSeries, fastlen, slowlen, siglen int64) (ValueSeries, ValueSeries, ValueSeries) { 29 | macdlineKey := fmt.Sprintf("macdline:%s:%d:%d:%d", src.ID(), fastlen, slowlen, siglen) 30 | macdline := getCache(macdlineKey) 31 | if macdline == nil { 32 | macdline = NewValueSeries() 33 | } 34 | 35 | signalLineKey := fmt.Sprintf("macdsignal:%s:%d:%d:%d", src.ID(), fastlen, slowlen, siglen) 36 | signalLine := getCache(signalLineKey) 37 | if signalLine == nil { 38 | signalLine = NewValueSeries() 39 | } 40 | 41 | macdHistogramKey := fmt.Sprintf("macdhistogram:%s:%d:%d:%d", src.ID(), fastlen, slowlen, siglen) 42 | macdHistogram := getCache(macdHistogramKey) 43 | if macdHistogram == nil { 44 | macdHistogram = NewValueSeries() 45 | } 46 | 47 | // current available value 48 | stop := src.GetCurrent() 49 | 50 | if stop == nil { 51 | return macdline, signalLine, macdHistogram 52 | } 53 | 54 | // latest value exists 55 | if macdHistogram.Get(stop.t) != nil { 56 | return macdline, signalLine, macdHistogram 57 | } 58 | 59 | fast := EMA(src, fastlen) 60 | 61 | slow := EMA(src, slowlen) 62 | 63 | macdline = Sub(fast, slow) 64 | macdline.SetCurrent(stop.t) 65 | 66 | signalLine = EMA(macdline, siglen) 67 | signalLine.SetCurrent(stop.t) 68 | 69 | macdHistogram = Sub(macdline, signalLine) 70 | macdHistogram.SetCurrent(stop.t) 71 | 72 | setCache(macdlineKey, macdline) 73 | setCache(signalLineKey, signalLine) 74 | setCache(macdHistogramKey, macdHistogram) 75 | 76 | return macdline, signalLine, macdHistogram 77 | } 78 | -------------------------------------------------------------------------------- /pine/series_macd_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // TestSeriesMACDNoData tests no data scenario 11 | // 12 | // t=time.Time (no iteration) | | 13 | // p=ValueSeries | | 14 | // ema=ValueSeries | | 15 | func TestSeriesMACDNoData(t *testing.T) { 16 | 17 | start := time.Now() 18 | data := OHLCVTestData(start, 4, 5*60*1000) 19 | 20 | series, err := NewOHLCVSeries(data) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | prop := OHLCVAttr(series, OHLCPropClose) 26 | mline, sigline, histline := MACD(prop, 12, 26, 9) 27 | if mline == nil { 28 | t.Error("Expected macdline to be non nil but got nil") 29 | } 30 | if sigline == nil { 31 | t.Error("Expected sigline to be non nil but got nil") 32 | } 33 | if histline == nil { 34 | t.Error("Expected histline to be non nil but got nil") 35 | } 36 | } 37 | 38 | // TestSeriesMACDNoIteration tests this sceneario where there's no iteration yet 39 | // 40 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 | 41 | // p=ValueSeries | 14 | 15 | 17 | 18 | 42 | // ema=ValueSeries | | | | | 43 | func TestSeriesMACDNoIteration(t *testing.T) { 44 | 45 | start := time.Now() 46 | data := OHLCVTestData(start, 4, 5*60*1000) 47 | data[0].C = 14 48 | data[1].C = 15 49 | data[2].C = 17 50 | data[3].C = 18 51 | 52 | series, err := NewOHLCVSeries(data) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | prop := OHLCVAttr(series, OHLCPropClose) 58 | mline, sigline, histline := MACD(prop, 12, 26, 9) 59 | if mline == nil { 60 | t.Error("Expected macdline to be non nil but got nil") 61 | } 62 | if sigline == nil { 63 | t.Error("Expected sigline to be non nil but got nil") 64 | } 65 | if histline == nil { 66 | t.Error("Expected histline to be non nil but got nil") 67 | } 68 | } 69 | 70 | // TestSeriesMACDIteration tests this scneario when the iterator is at t=4 is not at the end 71 | // 72 | // t=time.Time | 1 | 2 | 3 | 4 | 73 | // p=ValueSeries | 13 | 15 | 17 | 18 | 74 | // ema(close, 1) | 13 | 15 | 17 | 18 | 75 | // ema(close, 2) | nil | 14 | 16 | 17.3333 | 76 | // MACD line = ema(close, 1) - ema(close,2) | nil | 1 | 1 | 0.6667 | 77 | // Signal line = ema(ema(close, 1) - ema(close,2), 2) | nil | nil | 1 | 0.7778 | 78 | // MACD Histogram = MACD line - Signal line | nil | nil | 0 | -0.1111 | 79 | func TestSeriesMACDIteration(t *testing.T) { 80 | 81 | start := time.Now() 82 | data := OHLCVTestData(start, 4, 5*60*1000) 83 | data[0].C = 13 84 | data[1].C = 15 85 | data[2].C = 17 86 | data[3].C = 18 87 | 88 | series, err := NewOHLCVSeries(data) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | 93 | testTable := []struct { 94 | macd *float64 95 | signal *float64 96 | histogram *float64 97 | }{ 98 | { 99 | macd: nil, 100 | signal: nil, 101 | histogram: nil, 102 | }, 103 | { 104 | macd: NewFloat64(1), 105 | signal: nil, 106 | histogram: nil, 107 | }, 108 | { 109 | macd: NewFloat64(1), 110 | signal: NewFloat64(1), 111 | histogram: NewFloat64(0), 112 | }, 113 | { 114 | macd: NewFloat64(0.6667), 115 | signal: NewFloat64(0.7778), 116 | histogram: NewFloat64(-0.1111), 117 | }, 118 | } 119 | 120 | for i, v := range testTable { 121 | series.Next() 122 | src := OHLCVAttr(series, OHLCPropClose) 123 | macd, signal, histogram := MACD(src, 1, 2, 2) 124 | 125 | // macd line 126 | if (macd.Val() == nil) != (v.macd == nil) { 127 | if macd.Val() != nil { 128 | t.Fatalf("Expected macd to be nil: %t but got %+v for iteration: %d", v.macd == nil, *macd.Val(), i) 129 | } else { 130 | t.Fatalf("Expected macd to be nil: %t but got %+v for iteration: %d", v.macd == nil, macd.Val(), i) 131 | } 132 | } 133 | if v.macd != nil && fmt.Sprintf("%.04f", *v.macd) != fmt.Sprintf("%.04f", *macd.Val()) { 134 | t.Errorf("Expected macd to be %+v but got %+v for iteration: %d", *v.macd, *macd.Val(), i) 135 | } 136 | 137 | // signal line 138 | if (signal.Val() == nil) != (v.signal == nil) { 139 | if signal.Val() != nil { 140 | t.Fatalf("Expected signal to be nil: %t but got %+v for iteration: %d", v.signal == nil, *signal.Val(), i) 141 | } else { 142 | t.Fatalf("Expected signal to be nil: %t but got %+v for iteration: %d", v.signal == nil, signal.Val(), i) 143 | } 144 | } 145 | if v.signal != nil && fmt.Sprintf("%.04f", *v.signal) != fmt.Sprintf("%.04f", *signal.Val()) { 146 | t.Errorf("Expected signal to be %+v but got %+v for iteration: %d", *v.signal, *signal.Val(), i) 147 | } 148 | 149 | // macd histogram 150 | if (histogram.Val() == nil) != (v.histogram == nil) { 151 | if histogram.Val() != nil { 152 | t.Fatalf("Expected histogram to be nil: %t but got %+v for iteration: %d", v.histogram == nil, *histogram.Val(), i) 153 | } else { 154 | t.Fatalf("Expected histogram to be nil: %t but got %+v for iteration: %d", v.histogram == nil, histogram.Val(), i) 155 | } 156 | } 157 | if v.histogram != nil && fmt.Sprintf("%.04f", *v.histogram) != fmt.Sprintf("%.04f", *histogram.Val()) { 158 | t.Errorf("Expected histogram to be %+v but got %+v for iteration: %d", *v.histogram, *histogram.Val(), i) 159 | } 160 | } 161 | } 162 | 163 | func TestMemoryLeakMACD(t *testing.T) { 164 | testMemoryLeak(t, func(o OHLCVSeries) error { 165 | MACD(OHLCVAttr(o, OHLCPropClose), 12, 26, 9) 166 | return nil 167 | }) 168 | } 169 | 170 | func BenchmarkMACD(b *testing.B) { 171 | // run the Fib function b.N times 172 | start := time.Now() 173 | data := OHLCVTestData(start, 10000, 5*60*1000) 174 | series, _ := NewOHLCVSeries(data) 175 | vals := OHLCVAttr(series, OHLCPropClose) 176 | 177 | for n := 0; n < b.N; n++ { 178 | series.Next() 179 | MACD(vals, 12, 26, 9) 180 | } 181 | } 182 | 183 | func ExampleMACD() { 184 | start := time.Now() 185 | data := OHLCVTestData(start, 10000, 5*60*1000) 186 | series, _ := NewOHLCVSeries(data) 187 | close := OHLCVAttr(series, OHLCPropClose) 188 | mline, sigline, histline := MACD(close, 12, 26, 9) 189 | log.Printf("MACD line: %+v", mline.Val()) 190 | log.Printf("Signal line: %+v", sigline.Val()) 191 | log.Printf("Hist line: %+v", histline.Val()) 192 | } 193 | -------------------------------------------------------------------------------- /pine/series_mfi.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // MFI generates a ValueSeries of exponential moving average. 8 | func MFI(o OHLCVSeries, l int64) ValueSeries { 9 | key := fmt.Sprintf("mfi:%s:%d", o.ID(), l) 10 | mfi := getCache(key) 11 | if mfi == nil { 12 | mfi = NewValueSeries() 13 | } 14 | 15 | hlc3 := OHLCVAttr(o, OHLCPropHLC3) 16 | vol := OHLCVAttr(o, OHLCPropVolume) 17 | 18 | hlc3c := hlc3.GetCurrent() 19 | if hlc3c == nil { 20 | return mfi 21 | } 22 | 23 | chg := Change(hlc3, 1) 24 | 25 | u := OperateWithNil(hlc3, chg, "mfiu", func(a, b *Value) *Value { 26 | var v float64 27 | // treat nil value as HLC3 28 | if b == nil { 29 | return a 30 | } else { 31 | v = b.v 32 | } 33 | if v <= 0.0 { 34 | return &Value{ 35 | t: a.t, 36 | v: 0.0, 37 | } 38 | // NewFloat64(0.0) 39 | } 40 | return a 41 | }) 42 | lo := OperateWithNil(hlc3, chg, "mfil", func(a, b *Value) *Value { 43 | var v float64 44 | // treat nil value as HLC3 45 | if b == nil { 46 | return a 47 | } else { 48 | v = b.v 49 | } 50 | if v >= 0.0 { 51 | return &Value{ 52 | t: a.t, 53 | v: 0.0, 54 | } 55 | } 56 | return a 57 | }) 58 | 59 | uv := Mul(vol, u) 60 | lv := Mul(vol, lo) 61 | 62 | upper := Sum(uv, int(l)) 63 | 64 | lower := Sum(lv, int(l)) 65 | 66 | hundo := ReplaceAll(hlc3, 100) 67 | 68 | mfi = Sub(hundo, Div(hundo, AddConst(Div(upper, lower), 1))) 69 | 70 | mfi.SetCurrent(hlc3c.t) 71 | 72 | setCache(key, mfi) 73 | 74 | return mfi 75 | } 76 | -------------------------------------------------------------------------------- /pine/series_mfi_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // TestSeriesMFI tests no data scenario 11 | // 12 | // t=time.Time (no iteration) | | 13 | // p=ValueSeries | | 14 | // mfi=ValueSeries | | 15 | func TestSeriesMFI(t *testing.T) { 16 | 17 | start := time.Now() 18 | data := OHLCVTestData(start, 4, 5*60*1000) 19 | 20 | series, err := NewOHLCVSeries(data) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | mfi := MFI(series, 3) 26 | if mfi == nil { 27 | t.Error("Expected mfi to be non nil but got nil") 28 | } 29 | } 30 | 31 | // TestSeriesMFINoIteration tests this sceneario where there's no iteration yet 32 | // 33 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 | 34 | // p=ValueSeries | 14 | 15 | 17 | 18 | 35 | // mfi=ValueSeries | | | | | 36 | func TestSeriesMFINoIteration(t *testing.T) { 37 | 38 | start := time.Now() 39 | data := OHLCVTestData(start, 4, 5*60*1000) 40 | data[0].C = 14 41 | data[1].C = 15 42 | data[2].C = 17 43 | data[3].C = 18 44 | 45 | series, err := NewOHLCVSeries(data) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | mfi := MFI(series, 3) 51 | if mfi == nil { 52 | t.Error("Expected mfi to be non nil but got nil") 53 | } 54 | } 55 | 56 | // TestSeriesMFIIteration tests the output against TradingView's expected values 57 | func TestSeriesMFIIteration(t *testing.T) { 58 | data := OHLCVStaticTestData() 59 | series, err := NewOHLCVSeries(data) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | tests := []*float64{ 65 | nil, 66 | nil, 67 | nil, 68 | NewFloat64(38.856), 69 | NewFloat64(52.679), 70 | NewFloat64(27.212), 71 | NewFloat64(26.905), 72 | NewFloat64(28.794), 73 | NewFloat64(27.858), 74 | NewFloat64(31.572), 75 | } 76 | 77 | for i, v := range tests { 78 | series.Next() 79 | mfi := MFI(series, 4) 80 | 81 | // mfi line 82 | if (mfi.Val() == nil) != (v == nil) { 83 | if mfi.Val() != nil { 84 | t.Errorf("Expected mfi to be nil: %t but got %+v for iteration: %d", v == nil, *mfi.Val(), i) 85 | } else { 86 | t.Errorf("Expected mfi to be: %+v but got %+v for iteration: %d", *v, mfi.Val(), i) 87 | } 88 | continue 89 | } 90 | if v != nil && fmt.Sprintf("%.03f", *v) != fmt.Sprintf("%.03f", *mfi.Val()) { 91 | t.Errorf("Expected mfi to be %+v but got %+v for iteration: %d", *v, *mfi.Val(), i) 92 | } 93 | } 94 | } 95 | 96 | func TestMemoryLeakMFI(t *testing.T) { 97 | testMemoryLeak(t, func(o OHLCVSeries) error { 98 | MFI(o, 12) 99 | return nil 100 | }) 101 | } 102 | 103 | func BenchmarkMFI(b *testing.B) { 104 | // run the Fib function b.N times 105 | start := time.Now() 106 | data := OHLCVTestData(start, 10000, 5*60*1000) 107 | series, _ := NewOHLCVSeries(data) 108 | 109 | for n := 0; n < b.N; n++ { 110 | series.Next() 111 | MFI(series, 12) 112 | } 113 | } 114 | 115 | func ExampleMFI() { 116 | start := time.Now() 117 | data := OHLCVTestData(start, 10000, 5*60*1000) 118 | series, _ := NewOHLCVSeries(data) 119 | mfi := MFI(series, 12) 120 | log.Printf("MFI line: %+v", mfi.Val()) 121 | } 122 | -------------------------------------------------------------------------------- /pine/series_ohlcv_attr.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | ) 8 | 9 | func OHLCVAttr(o OHLCVSeries, p OHLCProp) ValueSeries { 10 | key := fmt.Sprintf("ohlcvattr:%s:%d", o.ID(), p) 11 | dest := getCache(key) 12 | if dest == nil { 13 | dest = NewValueSeries() 14 | } 15 | 16 | stop := o.Current() 17 | if stop == nil { 18 | return dest 19 | } 20 | dest = getOHLCVAttr(*stop, o, dest, p) 21 | 22 | dest.SetCurrent(stop.S) 23 | 24 | setCache(key, dest) 25 | 26 | return dest 27 | } 28 | 29 | func getOHLCVAttr(stop OHLCV, src OHLCVSeries, dest ValueSeries, p OHLCProp) ValueSeries { 30 | 31 | var startt time.Time 32 | 33 | firstVal := dest.GetLast() 34 | if firstVal != nil { 35 | if v := src.Get(firstVal.t); v != nil && v.next != nil { 36 | startt = v.next.S 37 | } 38 | // startt = firstVal.next.t 39 | } else if firstVal == nil { 40 | if v := src.GetFirst(); v != nil { 41 | startt = v.S 42 | } 43 | } 44 | 45 | if startt.IsZero() { 46 | return dest 47 | } 48 | 49 | ptr := startt 50 | 51 | for { 52 | v := src.Get(ptr) 53 | 54 | var propVal *float64 55 | switch p { 56 | case OHLCPropClose: 57 | propVal = NewFloat64(v.C) 58 | case OHLCPropOpen: 59 | propVal = NewFloat64(v.O) 60 | case OHLCPropHigh: 61 | propVal = NewFloat64(v.H) 62 | case OHLCPropLow: 63 | propVal = NewFloat64(v.L) 64 | case OHLCPropVolume: 65 | propVal = NewFloat64(v.V) 66 | case OHLCPropTR, OHLCPropTRHL: 67 | if v.prev != nil { 68 | p := v.prev 69 | v1 := math.Abs(v.H - v.L) 70 | v2 := math.Abs(v.H - p.C) 71 | v3 := math.Abs(v.L - p.C) 72 | v := math.Max(v1, math.Max(v2, v3)) 73 | propVal = NewFloat64(v) 74 | } 75 | if p == OHLCPropTRHL && v.prev == nil { 76 | d := v.H - v.L 77 | propVal = &d 78 | } 79 | case OHLCPropHLC3: 80 | propVal = NewFloat64((v.H + v.L + v.C) / 3) 81 | default: 82 | continue 83 | } 84 | if propVal != nil { 85 | dest.Set(v.S, *propVal) 86 | } 87 | 88 | if v.next == nil { 89 | break 90 | } 91 | if v.S.Equal(stop.S) { 92 | break 93 | } 94 | 95 | ptr = v.next.S 96 | } 97 | 98 | return dest 99 | } 100 | -------------------------------------------------------------------------------- /pine/series_ohlcv_attr_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestNewOHLCVGetSeries(t *testing.T) { 10 | start := time.Now() 11 | data := OHLCVTestData(start, 3, 5*60*1000) 12 | 13 | s, err := NewOHLCVSeries(data) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | testTable := []struct { 19 | prop []OHLCProp 20 | vals []float64 21 | }{ 22 | { 23 | prop: []OHLCProp{OHLCPropClose, OHLCPropHigh, OHLCPropLow, OHLCPropOpen, OHLCPropHLC3}, 24 | vals: []float64{data[0].C, data[0].H, data[0].L, data[0].O, (data[0].H + data[0].L + data[0].C) / 3}, 25 | }, 26 | { 27 | prop: []OHLCProp{OHLCPropClose, OHLCPropHigh, OHLCPropLow, OHLCPropOpen, OHLCPropHLC3}, 28 | vals: []float64{data[1].C, data[1].H, data[1].L, data[1].O, (data[1].H + data[1].L + data[1].C) / 3}, 29 | }, 30 | { 31 | prop: []OHLCProp{OHLCPropClose, OHLCPropHigh, OHLCPropLow, OHLCPropOpen, OHLCPropHLC3}, 32 | vals: []float64{data[2].C, data[2].H, data[2].L, data[2].O, (data[2].H + data[2].L + data[2].C) / 3}, 33 | }, 34 | } 35 | 36 | for i, v := range testTable { 37 | // move to next iteration 38 | s.Next() 39 | for j, p := range v.prop { 40 | vals := OHLCVAttr(s, p) 41 | val := vals.Val() 42 | if *val != v.vals[j] { 43 | t.Errorf("Expected %+v to bs %+v but got %+v for i: %d, j: %d", p, v.vals[j], val, i, j) 44 | } 45 | } 46 | } 47 | 48 | // if this is last, return nil 49 | if v, _ := s.Next(); v != nil { 50 | t.Errorf("Expected to be nil but got %+v", v) 51 | } 52 | } 53 | 54 | func TestNewOHLCVGetTrueRange(t *testing.T) { 55 | data := OHLCVStaticTestData() 56 | 57 | s, err := NewOHLCVSeries(data) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | 62 | expVals := []*float64{ 63 | nil, 64 | NewFloat64(6.8), 65 | NewFloat64(8.5), 66 | NewFloat64(7.9), 67 | NewFloat64(8.3), 68 | NewFloat64(6.3), 69 | NewFloat64(6.6), 70 | NewFloat64(9.6), 71 | NewFloat64(8.0), 72 | NewFloat64(7.6), 73 | } 74 | 75 | for i, v := range expVals { 76 | // move to next iteration 77 | s.Next() 78 | 79 | vals := OHLCVAttr(s, OHLCPropTR) 80 | 81 | if (vals.Val() == nil) != (v == nil) { 82 | t.Errorf("Expected %+v but got %+v for i: %d", v, vals.Val(), i) 83 | continue 84 | } 85 | if v == nil { 86 | continue 87 | } 88 | if fmt.Sprintf("%0.2f", *vals.Val()) != fmt.Sprintf("%0.2f", *v) { 89 | t.Errorf("expected %+v but got %+v", *v, *vals.Val()) 90 | } 91 | } 92 | 93 | // if this is last, return nil 94 | if v, _ := s.Next(); v != nil { 95 | t.Errorf("Expected to be nil but got %+v", v) 96 | } 97 | } 98 | 99 | func TestMemoryLeakOHLCVAttr(t *testing.T) { 100 | testMemoryLeak(t, func(o OHLCVSeries) error { 101 | OHLCVAttr(o, OHLCPropTR) 102 | return nil 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /pine/series_operate_nil.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func OperateWithNil(a, b ValueSeries, ns string, op func(a, b *Value) *Value) ValueSeries { 8 | key := fmt.Sprintf("operationwnil:%s:%s:%s", a.ID(), b.ID(), ns) 9 | dest := getCache(key) 10 | if dest == nil { 11 | dest = NewValueSeries() 12 | } 13 | 14 | f := a.GetFirst() 15 | for { 16 | if f == nil { 17 | break 18 | } 19 | 20 | newv := b.Get(f.t) 21 | 22 | if val := op(f, newv); val != nil { 23 | dest.Set(val.t, val.v) 24 | } 25 | 26 | f = f.next 27 | } 28 | 29 | propagateCurrent(b, dest) 30 | 31 | setCache(key, dest) 32 | 33 | return dest 34 | } 35 | -------------------------------------------------------------------------------- /pine/series_operation.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Operate operates on two series. Enabling caching means it starts from where it was left off. 8 | func Operate(a, b ValueSeries, ns string, op func(b, c float64) float64) ValueSeries { 9 | return operation(a, b, ns, op, true) 10 | } 11 | 12 | // OperateNoCache operates on two series without caching 13 | func OperateNoCache(a, b ValueSeries, ns string, op func(b, c float64) float64) ValueSeries { 14 | return operation(a, b, ns, op, false) 15 | } 16 | 17 | // operation operates on a and b ValueSeries using op function. use ns as a unique cache identifier 18 | func operation(a, b ValueSeries, ns string, op func(a, b float64) float64, cache bool) ValueSeries { 19 | key := fmt.Sprintf("operation:%s:%s:%s", a.ID(), b.ID(), ns) 20 | dest := getCache(key) 21 | if dest == nil { 22 | dest = NewValueSeries() 23 | } 24 | 25 | firstaVal := operationGetStart(a, dest) 26 | 27 | // nowhere to start 28 | if firstaVal == nil { 29 | 30 | // propagate current pointer if needed 31 | propagateCurrent(a, dest) 32 | 33 | return dest 34 | } 35 | 36 | f := firstaVal 37 | for { 38 | if f == nil { 39 | break 40 | } 41 | 42 | newv := b.Get(f.t) 43 | 44 | if newv != nil { 45 | dest.Set(f.t, op(f.v, newv.v)) 46 | } 47 | 48 | f = f.next 49 | } 50 | 51 | propagateCurrent(a, dest) 52 | 53 | if cache { 54 | setCache(key, dest) 55 | } 56 | 57 | return dest 58 | } 59 | 60 | func propagateCurrent(a, dest ValueSeries) { 61 | if cur := a.GetCurrent(); cur != nil { 62 | dest.SetCurrent(cur.t) 63 | } 64 | } 65 | 66 | func operationGetStart(a, dest ValueSeries) *Value { 67 | var firstaVal *Value 68 | destlast := dest.GetLast() 69 | if destlast == nil { 70 | firstaVal = a.GetFirst() 71 | } else if destlast != nil { 72 | if v := a.Get(destlast.t); v != nil && v.next != nil { 73 | firstaVal = v.next 74 | } 75 | } 76 | return firstaVal 77 | } 78 | -------------------------------------------------------------------------------- /pine/series_operation_const.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // operationConst operates on a and b ValueSeries using op function. use ns as a unique cache identifier 8 | func operationConst(a ValueSeries, ns string, op func(a float64) float64, cache bool) ValueSeries { 9 | key := fmt.Sprintf("operationconst:%s:%s", a.ID(), ns) 10 | dest := getCache(key) 11 | if dest == nil { 12 | dest = NewValueSeries() 13 | } 14 | 15 | firstaVal := operationGetStart(a, dest) 16 | 17 | // nowhere to start 18 | if firstaVal == nil { 19 | // propagate current pointer if needed 20 | propagateCurrent(a, dest) 21 | 22 | return dest 23 | } 24 | 25 | f := firstaVal 26 | for { 27 | if f == nil { 28 | break 29 | } 30 | 31 | dest.Set(f.t, op(f.v)) 32 | 33 | f = f.next 34 | } 35 | 36 | propagateCurrent(a, dest) 37 | 38 | if cache { 39 | setCache(key, dest) 40 | } 41 | 42 | return dest 43 | } 44 | -------------------------------------------------------------------------------- /pine/series_pow.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Pow generates a ValueSeries of values from power function 9 | // 10 | // Parameters 11 | // - p - ValueSeries: source data 12 | // - exp - float64: exponent of the power function 13 | func Pow(src ValueSeries, exp float64) ValueSeries { 14 | key := fmt.Sprintf("pow:%s:%.8f", src.ID(), exp) 15 | pow := getCache(key) 16 | if pow == nil { 17 | pow = NewValueSeries() 18 | } 19 | 20 | // current available value 21 | stop := src.GetCurrent() 22 | if stop == nil { 23 | return pow 24 | } 25 | 26 | pow = getPow(*stop, pow, src, exp) 27 | // disable this for now 28 | // setCache(key, pow) 29 | 30 | pow.SetCurrent(stop.t) 31 | 32 | return pow 33 | } 34 | 35 | func getPow(stop Value, pow ValueSeries, src ValueSeries, exp float64) ValueSeries { 36 | 37 | var startNew *Value 38 | 39 | lastAvail := pow.GetLast() 40 | 41 | if lastAvail == nil { 42 | startNew = src.GetFirst() 43 | } else { 44 | v := src.Get(lastAvail.t) 45 | startNew = v.next 46 | } 47 | 48 | if startNew == nil { 49 | // if nothing is to start with, then nothing can be done 50 | return pow 51 | } 52 | 53 | // first new time 54 | itervt := startNew.t 55 | 56 | for { 57 | v := src.Get(itervt) 58 | if v == nil { 59 | break 60 | } 61 | newpow := math.Pow(v.v, exp) 62 | pow.Set(itervt, newpow) 63 | 64 | if v.next == nil { 65 | break 66 | } 67 | if v.t.Equal(stop.t) { 68 | break 69 | } 70 | 71 | itervt = v.next.t 72 | } 73 | 74 | return pow 75 | } 76 | -------------------------------------------------------------------------------- /pine/series_pow_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // TestSeriesPowNoData tests no data scenario 11 | // 12 | // t=time.Time (no iteration) | | 13 | // p=ValueSeries | | 14 | // stdev=ValueSeries | | 15 | func TestSeriesPowNoData(t *testing.T) { 16 | 17 | start := time.Now() 18 | data := OHLCVTestData(start, 4, 5*60*1000) 19 | 20 | series, err := NewOHLCVSeries(data) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | prop := OHLCVAttr(series, OHLCPropClose) 26 | stdev := Pow(prop, 2.0) 27 | if stdev == nil { 28 | t.Error("Expected to be non nil but got nil") 29 | } 30 | } 31 | 32 | // TestSeriesPowNoIteration tests this sceneario where there's no iteration yet 33 | // 34 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 | 35 | // p=ValueSeries | 14 | 15 | 17 | 18 | 36 | // pow=ValueSeries | | | | | 37 | func TestSeriesPowNoIteration(t *testing.T) { 38 | 39 | start := time.Now() 40 | data := OHLCVTestData(start, 4, 5*60*1000) 41 | data[0].C = 14 42 | data[1].C = 15 43 | data[2].C = 17 44 | data[3].C = 18 45 | 46 | series, err := NewOHLCVSeries(data) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | prop := OHLCVAttr(series, OHLCPropClose) 52 | pow := Pow(prop, 2) 53 | if pow == nil { 54 | t.Error("Expected to be non-nil but got nil") 55 | } 56 | } 57 | 58 | // TestSeriesPowIteration tests this scneario 59 | // 60 | // t=time.Time | 1 | 2 | 3 | 61 | // p=ValueSeries | 13 | 15 | 11 | 62 | // pow(0.5) | 3.606 | 3.873 | 3.317 | 63 | // pow(2) | 169 | 225 | 121 | 64 | func TestSeriesPowIteration(t *testing.T) { 65 | 66 | start := time.Now() 67 | data := OHLCVTestData(start, 3, 5*60*1000) 68 | data[0].C = 13 69 | data[1].C = 15 70 | data[2].C = 11 71 | 72 | series, err := NewOHLCVSeries(data) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | testTable := []struct { 78 | exp float64 79 | vals []float64 80 | }{ 81 | { 82 | exp: 0.5, 83 | vals: []float64{3.606, 3.873, 3.317}, 84 | }, 85 | { 86 | exp: 2, 87 | vals: []float64{169, 225, 121}, 88 | }, 89 | } 90 | 91 | for j := 0; j <= 2; j++ { 92 | series.Next() 93 | 94 | for i, v := range testTable { 95 | prop := OHLCVAttr(series, OHLCPropClose) 96 | pow := Pow(prop, v.exp) 97 | exp := v.vals[j] 98 | if exp == 0 { 99 | if pow.Val() != nil { 100 | t.Fatalf("expected nil but got non nil: %+v at vals item: %d, testtable item: %d", *pow.Val(), j, i) 101 | } 102 | // OK 103 | } 104 | if exp != 0 { 105 | if pow.Val() == nil { 106 | t.Fatalf("expected non nil: %+v but got nil at vals item: %d, testtable item: %d", exp, j, i) 107 | } 108 | if fmt.Sprintf("%.03f", exp) != fmt.Sprintf("%.03f", *pow.Val()) { 109 | t.Fatalf("expected %+v but got %+v at vals item: %d, testtable item: %d", exp, *pow.Val(), j, i) 110 | } 111 | // OK 112 | } 113 | } 114 | } 115 | } 116 | 117 | func TestMemoryLeakPow(t *testing.T) { 118 | testMemoryLeak(t, func(o OHLCVSeries) error { 119 | prop := OHLCVAttr(o, OHLCPropClose) 120 | Pow(prop, 2) 121 | return nil 122 | }) 123 | } 124 | 125 | func ExamplePow() { 126 | start := time.Now() 127 | data := OHLCVTestData(start, 10000, 5*60*1000) 128 | series, _ := NewOHLCVSeries(data) 129 | for { 130 | if v, _ := series.Next(); v == nil { 131 | break 132 | } 133 | 134 | close := OHLCVAttr(series, OHLCPropClose) 135 | added := AddConst(close, 3.0) 136 | pow := Pow(added, 2) 137 | log.Printf("Pow: %+v", pow.Val()) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /pine/series_rma.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // RMA generates a ValueSeries of the exponentially weighted moving average with alpha = 1 / length. 8 | // This is equivalent to J. Welles Wilder's smoothed moving average. 9 | func RMA(p ValueSeries, l int64) ValueSeries { 10 | key := fmt.Sprintf("rma:%s:%d", p.ID(), l) 11 | rma := getCache(key) 12 | if rma == nil { 13 | rma = NewValueSeries() 14 | } 15 | 16 | if p == nil || p.GetCurrent() == nil { 17 | return rma 18 | } 19 | 20 | // current available value 21 | stop := p.GetCurrent() 22 | 23 | rma = getRMA(stop, p, rma, l) 24 | 25 | setCache(key, rma) 26 | 27 | rma.SetCurrent(stop.t) 28 | 29 | return rma 30 | } 31 | 32 | func getRMA(stop *Value, vs ValueSeries, rma ValueSeries, l int64) ValueSeries { 33 | 34 | var mul float64 = 1.0 / float64(l) 35 | firstVal := rma.GetLast() 36 | 37 | if firstVal == nil { 38 | firstVal = vs.GetFirst() 39 | } 40 | 41 | if firstVal == nil { 42 | // if nothing is available, then nothing can be done 43 | return rma 44 | } 45 | 46 | itervt := firstVal.t 47 | 48 | var fseek int64 49 | var ftot float64 50 | 51 | for { 52 | v := vs.Get(itervt) 53 | if v == nil { 54 | break 55 | } 56 | e := rma.Get(itervt) 57 | if e != nil && v.next == nil { 58 | break 59 | } 60 | if e != nil { 61 | itervt = v.next.t 62 | continue 63 | } 64 | 65 | // get previous rma 66 | if v.prev != nil { 67 | prevv := vs.Get(v.prev.t) 68 | preve := rma.Get(prevv.t) 69 | // previous ema exists, just do multiplication to that 70 | if preve != nil { 71 | nextRMA := (preve.v)*(1-mul) + v.v*mul 72 | rma.Set(v.t, nextRMA) 73 | continue 74 | } 75 | } 76 | 77 | // previous value does not exist. just keep adding until multplication is required 78 | fseek++ 79 | ftot = ftot + v.v 80 | 81 | if fseek == l { 82 | avg := ftot / float64(fseek) 83 | rma.Set(v.t, avg) 84 | } 85 | 86 | if v.next == nil { 87 | break 88 | } 89 | if v.t.Equal(stop.t) { 90 | break 91 | } 92 | itervt = v.next.t 93 | } 94 | 95 | return rma 96 | } 97 | -------------------------------------------------------------------------------- /pine/series_rma_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // TestSeriesRMANoData tests no data scenario 10 | // 11 | // t=time.Time (no iteration) | | 12 | // p=ValueSeries | | 13 | // rma=ValueSeries | | 14 | func TestSeriesRMANoData(t *testing.T) { 15 | 16 | start := time.Now() 17 | data := OHLCVTestData(start, 4, 5*60*1000) 18 | 19 | series, err := NewOHLCVSeries(data) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | prop := OHLCVAttr(series, OHLCPropClose) 25 | rma := RMA(prop, 2) 26 | if rma == nil { 27 | t.Error("Expected to be non nil but got nil") 28 | } 29 | } 30 | 31 | // TestSeriesRMANoIteration tests this sceneario where there's no iteration yet 32 | // 33 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 | 34 | // p=ValueSeries | 14 | 15 | 17 | 18 | 35 | // rma=ValueSeries | | | | | 36 | func TestSeriesRMANoIteration(t *testing.T) { 37 | 38 | start := time.Now() 39 | data := OHLCVTestData(start, 4, 5*60*1000) 40 | data[0].C = 14 41 | data[1].C = 15 42 | data[2].C = 17 43 | data[3].C = 18 44 | 45 | series, err := NewOHLCVSeries(data) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | prop := OHLCVAttr(series, OHLCPropClose) 51 | rma := RMA(prop, 2) 52 | if rma == nil { 53 | t.Error("Expected to be non-nil but got nil") 54 | } 55 | } 56 | 57 | // TestSeriesRMAIteration4 tests this scneario when the iterator is at t=4 is not at the end 58 | // 59 | // t=time.Time | 1 | 2 | 3 | 4 (time here) | 60 | // p=ValueSeries | 13 | 15 | 17 | 18 | 61 | // rma(close, 1) | 13 | 15 | 17 | 18 | 62 | // rma(close, 2) | nil | 14 | 15.5 | 16.75 | 63 | // rma(close, 3) | nil | nil | 15 | 16 | 64 | // rma(close, 4) | nil | nil | nil | 15.75 | 65 | // rma(close, 5) | nil | nil | nil | nil | 66 | func TestSeriesRMAIteration4(t *testing.T) { 67 | 68 | start := time.Now() 69 | data := OHLCVTestData(start, 4, 5*60*1000) 70 | data[0].C = 13 71 | data[1].C = 15 72 | data[2].C = 17 73 | data[3].C = 18 74 | 75 | series, err := NewOHLCVSeries(data) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | series.Next() 81 | series.Next() 82 | series.Next() 83 | series.Next() 84 | 85 | testTable := []struct { 86 | lookback int 87 | exp float64 88 | isNil bool 89 | }{ 90 | { 91 | lookback: 1, 92 | exp: 18, 93 | }, 94 | { 95 | lookback: 2, 96 | exp: 16.75, 97 | }, 98 | { 99 | lookback: 3, 100 | exp: 16, 101 | }, 102 | { 103 | lookback: 4, 104 | exp: 15.75, 105 | }, 106 | { 107 | lookback: 5, 108 | exp: 0, 109 | isNil: true, 110 | }, 111 | } 112 | 113 | for i, v := range testTable { 114 | prop := OHLCVAttr(series, OHLCPropClose) 115 | rma := RMA(prop, int64(v.lookback)) 116 | 117 | if rma == nil { 118 | t.Errorf("Expected to be non nil but got nil at idx: %d", i) 119 | } 120 | if v.isNil && rma.Val() != nil { 121 | t.Error("expected to be nil but got non nil") 122 | } 123 | if !v.isNil && *rma.Val() != v.exp { 124 | t.Errorf("Expected to get %+v but got %+v for lookback %+v", v.exp, *rma.Val(), v.lookback) 125 | } 126 | } 127 | } 128 | 129 | // TestSeriesRMAIteration3 tests this scneario when the iterator is at t=4 is not at the end 130 | // 131 | // t=time.Time | 1 | 2 | 3 (time here) | 4 | 132 | // p=ValueSeries | 13 | 15 | 17 | 18 | 133 | // rma(close, 1) | 13 | 15 | 17 | 18 | 134 | // rma(close, 2) | nil | 14 | 15.5 | 16.75 | 135 | // rma(close, 3) | nil | nil | 15 | 16 | 136 | // rma(close, 4) | nil | nil | nil | 15.75 | 137 | // rma(close, 5) | nil | nil | nil | nil | 138 | func TestSeriesRMAIteration3(t *testing.T) { 139 | 140 | start := time.Now() 141 | data := OHLCVTestData(start, 4, 5*60*1000) 142 | data[0].C = 13 143 | data[1].C = 15 144 | data[2].C = 17 145 | data[3].C = 18 146 | 147 | series, err := NewOHLCVSeries(data) 148 | if err != nil { 149 | t.Fatal(err) 150 | } 151 | 152 | series.Next() 153 | series.Next() 154 | series.Next() 155 | 156 | testTable := []struct { 157 | lookback int 158 | exp float64 159 | isNil bool 160 | }{ 161 | { 162 | lookback: 1, 163 | exp: 17, 164 | }, 165 | { 166 | lookback: 2, 167 | exp: 15.5, 168 | }, 169 | { 170 | lookback: 3, 171 | exp: 15, 172 | }, 173 | { 174 | lookback: 4, 175 | exp: 0, 176 | isNil: true, 177 | }, 178 | { 179 | lookback: 5, 180 | exp: 0, 181 | isNil: true, 182 | }, 183 | } 184 | 185 | for i, v := range testTable { 186 | prop := OHLCVAttr(series, OHLCPropClose) 187 | rma := RMA(prop, int64(v.lookback)) 188 | 189 | if rma == nil { 190 | t.Errorf("Expected to be non nil but got nil at idx: %d", i) 191 | } 192 | if v.isNil && rma.Val() != nil { 193 | t.Error("expected to be nil but got non nil") 194 | } 195 | if !v.isNil && *rma.Val() != v.exp { 196 | t.Errorf("Expected to get %+v but got %+v for lookback %+v", v.exp, *rma.Val(), v.lookback) 197 | } 198 | } 199 | } 200 | 201 | // TestSeriesRMANested tests nested RMA 202 | // 203 | // t=time.Time | 1 | 2 | 3 (time here 1) | 4 (time here2) 204 | // p=ValueSeries | 13 | 15 | 17 | 18 205 | // rma(close, 2) | nil | 14 | 15.5 | 16.75 206 | // rma(rma(close, 2), 2) | nil | nil | 14.75 | 15.75 207 | func TestSeriesRMANested(t *testing.T) { 208 | 209 | start := time.Now() 210 | data := OHLCVTestData(start, 5, 5*60*1000) 211 | data[0].C = 13 212 | data[1].C = 15 213 | data[2].C = 17 214 | data[3].C = 18 215 | 216 | series, err := NewOHLCVSeries(data) 217 | if err != nil { 218 | t.Fatal(err) 219 | } 220 | 221 | series.Next() 222 | series.Next() 223 | series.Next() 224 | 225 | testTable := []float64{14.75, 15.75} 226 | 227 | for _, v := range testTable { 228 | prop := OHLCVAttr(series, OHLCPropClose) 229 | 230 | rma := RMA(prop, 2) 231 | rma2 := RMA(rma, 2) 232 | if *rma2.Val() != v { 233 | t.Errorf("expectd %+v but got %+v", v, *rma2.Val()) 234 | } 235 | series.Next() 236 | } 237 | } 238 | 239 | // TestSeriesRMANotEnoughData tests this scneario when the lookback is more than the number of data available 240 | // 241 | // t=time.Time | 1 | 2 | 3 | 4 (here) | 242 | // p=ValueSeries | 14 | 15 | 17 | 18 | 243 | // rma(close, 5) | nil| nil | nil| nil | 244 | func TestSeriesRMANotEnoughData(t *testing.T) { 245 | 246 | start := time.Now() 247 | data := OHLCVTestData(start, 4, 5*60*1000) 248 | data[0].C = 15 249 | data[1].C = 16 250 | data[2].C = 17 251 | data[3].C = 18 252 | 253 | log.Printf("Data[0].S, %+v, 3s: %+v", data[0].S, data[3].S) 254 | 255 | series, err := NewOHLCVSeries(data) 256 | if err != nil { 257 | t.Fatal(err) 258 | } 259 | 260 | series.Next() 261 | series.Next() 262 | series.Next() 263 | series.Next() 264 | 265 | testTable := []struct { 266 | lookback int 267 | exp *float64 268 | }{ 269 | { 270 | lookback: 5, 271 | exp: nil, 272 | }, 273 | { 274 | lookback: 6, 275 | exp: nil, 276 | }, 277 | } 278 | 279 | for i, v := range testTable { 280 | prop := OHLCVAttr(series, OHLCPropClose) 281 | 282 | rma := RMA(prop, int64(v.lookback)) 283 | if rma == nil { 284 | t.Errorf("Expected to be non nil but got nil at idx: %d", i) 285 | } 286 | if rma.Val() != v.exp { 287 | t.Errorf("Expected to get %+v but got %+v for lookback %+v", v.exp, rma, v.lookback) 288 | } 289 | } 290 | } 291 | 292 | func TestMemoryLeakRMA(t *testing.T) { 293 | testMemoryLeak(t, func(o OHLCVSeries) error { 294 | prop := OHLCVAttr(o, OHLCPropClose) 295 | RMA(prop, 12) 296 | return nil 297 | }) 298 | } 299 | 300 | func ExampleRMA() { 301 | start := time.Now() 302 | data := OHLCVTestData(start, 10000, 5*60*1000) 303 | series, _ := NewOHLCVSeries(data) 304 | for { 305 | if v, _ := series.Next(); v == nil { 306 | break 307 | } 308 | 309 | close := OHLCVAttr(series, OHLCPropClose) 310 | rma := RMA(close, 12) 311 | log.Printf("RMA: %+v", rma.Val()) 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /pine/series_roc.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ROC calculates the percentage of change (rate of change) between the current value of `source` and its value `length` bars ago. 8 | // It is calculated by the formula 9 | // 10 | // - 100 * change(src, length) / src[length]. 11 | // 12 | // arguments are 13 | // - src: ValueSeries - Source data 14 | // - length: int - number of bars to lookback. 1 is the previous bar 15 | func ROC(src ValueSeries, l int) ValueSeries { 16 | 17 | key := fmt.Sprintf("roc:%s:%s:%d", src.ID(), src.ID(), l) 18 | rocs := getCache(key) 19 | if rocs == nil { 20 | rocs = NewValueSeries() 21 | } 22 | 23 | // current available value 24 | stop := src.GetCurrent() 25 | 26 | if stop == nil { 27 | return rocs 28 | } 29 | 30 | chg := Change(src, l) 31 | 32 | rocs = roc(*stop, src, rocs, chg, l) 33 | 34 | setCache(key, rocs) 35 | 36 | rocs.SetCurrent(stop.t) 37 | 38 | return rocs 39 | } 40 | 41 | func roc(stop Value, src, roc, chg ValueSeries, l int) ValueSeries { 42 | 43 | var val *Value 44 | 45 | lastvw := roc.GetCurrent() 46 | if lastvw != nil { 47 | val = src.Get(lastvw.t) 48 | if val != nil { 49 | val = val.next 50 | } 51 | } else { 52 | val = src.GetFirst() 53 | } 54 | 55 | if val == nil { 56 | return roc 57 | } 58 | 59 | // populate src values 60 | condSrc := make([]float64, 0) 61 | 62 | prevVal := val 63 | for { 64 | prevVal = prevVal.prev 65 | if prevVal == nil { 66 | break 67 | } 68 | 69 | b := src.Get(prevVal.t) 70 | if b == nil { 71 | continue 72 | } 73 | 74 | srcv := src.Get(prevVal.t) 75 | // add at the beginning since we go backwards 76 | condSrc = append([]float64{srcv.v}, condSrc...) 77 | 78 | if len(condSrc) == (l + 1) { 79 | break 80 | } 81 | } 82 | 83 | // last available does not exist. start from first 84 | 85 | for { 86 | if val == nil { 87 | break 88 | } 89 | // update 90 | 91 | srcval := src.Get(val.t) 92 | if srcval != nil { 93 | condSrc = append(condSrc, srcval.v) 94 | if len(condSrc) > (l + 1) { 95 | condSrc = condSrc[1:] 96 | } 97 | } 98 | 99 | if len(condSrc) == (l + 1) { 100 | vwappend := condSrc[0] 101 | chgv := chg.Get(val.t) 102 | if chgv != nil { 103 | v := 100 * chgv.v / vwappend 104 | roc.Set(val.t, v) 105 | } 106 | } 107 | 108 | val = val.next 109 | } 110 | 111 | return roc 112 | } 113 | -------------------------------------------------------------------------------- /pine/series_roc_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // TestSeriesROCNoData tests no data scenario 11 | // 12 | // t=time.Time (no iteration) | | 13 | // p=ValueSeries | | 14 | // change=ValueSeries | | 15 | func TestSeriesROCNoData(t *testing.T) { 16 | 17 | start := time.Now() 18 | data := OHLCVTestData(start, 0, 5*60*1000) 19 | 20 | series, err := NewOHLCVSeries(data) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | src := OHLCVAttr(series, OHLCPropClose) 26 | 27 | rsi := ROC(src, 2) 28 | if rsi == nil { 29 | t.Error("Expected to be non nil but got nil") 30 | } 31 | } 32 | 33 | // TestSeriesROCNoIteration tests this sceneario where there's no iteration yet 34 | // 35 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 36 | // src=ValueSeries | 11 | 14 | 12 | 13 37 | // roc(src, 1) | nil | 27.2727 | -14.286 | 8.3333 38 | // roc(src, 2) | nil | nil | 9.090909 | 7.1429 39 | // roc(src, 3) | nil | nil | nil | 18.1818 40 | func TestSeriesROCNoIteration(t *testing.T) { 41 | 42 | start := time.Now() 43 | data := OHLCVTestData(start, 4, 5*60*1000) 44 | data[0].C = 11 45 | data[1].C = 14 46 | data[2].C = 12 47 | data[3].C = 13 48 | 49 | series, err := NewOHLCVSeries(data) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | src := OHLCVAttr(series, OHLCPropClose) 55 | rsi := ROC(src, 1) 56 | if rsi == nil { 57 | t.Error("Expected to be non-nil but got nil") 58 | } 59 | } 60 | 61 | // TestSeriesROCSuccess tests this scneario when the iterator is at t=4 is not at the end 62 | // 63 | // t=time.Time | 1 | 2 | 3 | 4 64 | // src=ValueSeries | 11 | 14 | 12 | 13 65 | // roc(src, 1) | nil | 27.2727 | -14.2857 | 8.3333 66 | // roc(src, 2) | nil | nil | 9.090909 | -7.1429 67 | // roc(src, 3) | nil | nil | nil | 18.1818 68 | func TestSeriesROCSuccess(t *testing.T) { 69 | 70 | start := time.Now() 71 | data := OHLCVTestData(start, 4, 5*60*1000) 72 | data[0].C = 11 73 | data[1].C = 14 74 | data[2].C = 12 75 | data[3].C = 13 76 | 77 | series, err := NewOHLCVSeries(data) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | testTable := []struct { 83 | lookback int 84 | vals []float64 85 | }{ 86 | { 87 | lookback: 1, 88 | vals: []float64{0, 27.2727, -14.2857, 8.3333}, 89 | }, 90 | { 91 | lookback: 2, 92 | vals: []float64{0, 0, 9.090909, -7.1429}, 93 | }, 94 | { 95 | lookback: 3, 96 | vals: []float64{0, 0, 0, 18.1818}, 97 | }, 98 | } 99 | 100 | for j := 0; j <= 3; j++ { 101 | series.Next() 102 | 103 | for i, v := range testTable { 104 | src := OHLCVAttr(series, OHLCPropClose) 105 | vw := ROC(src, v.lookback) 106 | exp := v.vals[j] 107 | if exp == 0 { 108 | if vw.Val() != nil { 109 | t.Fatalf("expected nil but got non nil: %+v at vals item: %d, testtable item: %d", *vw.Val(), j, i) 110 | } 111 | // OK 112 | } 113 | if exp != 0 { 114 | if vw.Val() == nil { 115 | t.Fatalf("expected non nil: %+v but got nil at vals item: %d, testtable item: %d", exp, j, i) 116 | } 117 | if fmt.Sprintf("%.4f", exp) != fmt.Sprintf("%.4f", *vw.Val()) { 118 | t.Fatalf("expected %+v but got %+v at vals item: %d, testtable item: %d", exp, *vw.Val(), j, i) 119 | } 120 | // OK 121 | } 122 | } 123 | } 124 | } 125 | 126 | // TestSeriesROCNotEnoughData tests this scneario when the lookback is more than the number of data available 127 | // 128 | // t=time.Time | 1 | 2 | 3 | 4 129 | // src=ValueSeries | 11 | 14 | 12 | 13 130 | // roc(src, 1) | nil | 27.2727 | -14.2857 | 8.3333 131 | // roc(src, 2) | nil | nil | 9.090909 | -7.1429 132 | // roc(src, 3) | nil | nil | nil | 18.1818 133 | func TestSeriesROCNotEnoughData(t *testing.T) { 134 | 135 | start := time.Now() 136 | data := OHLCVTestData(start, 4, 5*60*1000) 137 | data[0].C = 13 138 | data[1].C = 15 139 | data[2].C = 11 140 | data[3].C = 18 141 | 142 | series, err := NewOHLCVSeries(data) 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | 147 | series.Next() 148 | series.Next() 149 | series.Next() 150 | series.Next() 151 | 152 | src := OHLCVAttr(series, OHLCPropClose) 153 | 154 | vw := ROC(src, 4) 155 | if vw.Val() != nil { 156 | t.Errorf("Expected nil but got %+v", *vw.Val()) 157 | } 158 | } 159 | 160 | func TestMemoryLeakROC(t *testing.T) { 161 | testMemoryLeak(t, func(o OHLCVSeries) error { 162 | prop := OHLCVAttr(o, OHLCPropClose) 163 | ROC(prop, 3) 164 | return nil 165 | }) 166 | } 167 | 168 | func BenchmarkROC(b *testing.B) { 169 | // run the Fib function b.N times 170 | start := time.Now() 171 | data := OHLCVTestData(start, 10000, 5*60*1000) 172 | series, _ := NewOHLCVSeries(data) 173 | vals := OHLCVAttr(series, OHLCPropClose) 174 | 175 | for n := 0; n < b.N; n++ { 176 | series.Next() 177 | ROC(vals, 5) 178 | } 179 | } 180 | 181 | func ExampleROC() { 182 | start := time.Now() 183 | data := OHLCVTestData(start, 10000, 5*60*1000) 184 | series, _ := NewOHLCVSeries(data) 185 | for { 186 | if v, _ := series.Next(); v == nil { 187 | break 188 | } 189 | 190 | close := OHLCVAttr(series, OHLCPropClose) 191 | roc := ROC(close, 4) 192 | log.Printf("ROC: %+v", roc.Val()) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /pine/series_rsi.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // RSI generates a ValueSeries of relative strength index 9 | // 10 | // The formula for RSI is 11 | // - u = Count the number of p(t+1) - p(t) > 0 as gains 12 | // - d = Count the number of p(t+1) - p(t) < 0 as losses 13 | // - rs = ta.rma(u) / ta.rma(d) 14 | // - res = 100 - 100 / (1 + rs) 15 | func RSI(p ValueSeries, l int64) ValueSeries { 16 | key := fmt.Sprintf("rsi:%s:%d", p.ID(), l) 17 | rsi := getCache(key) 18 | if rsi == nil { 19 | rsi = NewValueSeries() 20 | } 21 | 22 | if p == nil || p.GetCurrent() == nil { 23 | return rsi 24 | } 25 | 26 | // current available value 27 | stop := p.GetCurrent() 28 | 29 | rsi = getRSI(stop, p, rsi, l) 30 | 31 | setCache(key, rsi) 32 | 33 | rsi.SetCurrent(stop.t) 34 | 35 | return rsi 36 | } 37 | 38 | // getRSIU generates sum gains 39 | func getRSIU(stop *Value, vs ValueSeries, rsiu ValueSeries, l int64) ValueSeries { 40 | firstVal := rsiu.GetLast() 41 | 42 | if firstVal == nil { 43 | firstVal = vs.GetFirst() 44 | } 45 | 46 | if firstVal == nil { 47 | // if nothing is available, then nothing can be done 48 | return rsiu 49 | } 50 | 51 | itervt := firstVal.t 52 | 53 | var fseek int64 54 | var ftot float64 55 | 56 | for { 57 | v := vs.Get(itervt) 58 | if v == nil { 59 | break 60 | } 61 | e := rsiu.Get(itervt) 62 | if e != nil && v.next == nil { 63 | break 64 | } 65 | if e != nil { 66 | itervt = v.next.t 67 | continue 68 | } 69 | 70 | // get previous value 71 | if v.prev != nil { 72 | prevv := v.prev 73 | // previous value exists 74 | if prevv != nil { 75 | prevr := rsiu.Get(prevv.t) 76 | 77 | // previous rsiu exists 78 | if prevr != nil { 79 | prevFirstVal := prevv 80 | removelb := 1 81 | for i := 1; i < int(l)+1; i++ { 82 | if prevFirstVal.prev == nil { 83 | break 84 | } 85 | removelb++ 86 | prevFirstVal = prevFirstVal.prev 87 | } 88 | 89 | // was able to find previous value 90 | if int64(removelb) == l+1 { 91 | toAdd := math.Max(v.v-v.prev.v, 0) 92 | remval := math.Max(prevFirstVal.next.v-prevFirstVal.v, 0) 93 | newrsiu := prevr.v - remval + toAdd 94 | rsiu.Set(v.t, newrsiu) 95 | continue 96 | } 97 | } 98 | } 99 | } 100 | 101 | // previous rsiu does not exist. just keep adding until multiplication is required 102 | fseek++ 103 | if v.prev != nil { 104 | ftot = ftot + math.Max(v.v-v.prev.v, 0) 105 | } 106 | 107 | if fseek == l+1 { 108 | rsiu.Set(v.t, ftot) 109 | } 110 | 111 | if v.next == nil { 112 | break 113 | } 114 | if v.t.Equal(stop.t) { 115 | break 116 | } 117 | itervt = v.next.t 118 | } 119 | 120 | return rsiu 121 | } 122 | 123 | // getRSID generates sum gains 124 | func getRSID(stop *Value, vs ValueSeries, rsid ValueSeries, l int64) ValueSeries { 125 | firstVal := rsid.GetLast() 126 | 127 | if firstVal == nil { 128 | firstVal = vs.GetFirst() 129 | } 130 | 131 | if firstVal == nil { 132 | // if nothing is available, then nothing can be done 133 | return rsid 134 | } 135 | 136 | itervt := firstVal.t 137 | 138 | var fseek int64 139 | var ftot float64 140 | 141 | for { 142 | v := vs.Get(itervt) 143 | if v == nil { 144 | break 145 | } 146 | e := rsid.Get(itervt) 147 | if e != nil && v.next == nil { 148 | break 149 | } 150 | if e != nil { 151 | itervt = v.next.t 152 | continue 153 | } 154 | 155 | // get previous value 156 | if v.prev != nil { 157 | prevv := v.prev 158 | // previous value exists 159 | if prevv != nil { 160 | prevr := rsid.Get(prevv.t) 161 | 162 | // previous rsiu exists 163 | if prevr != nil { 164 | prevFirstVal := prevv 165 | removelb := 1 166 | for i := 1; i < int(l)+1; i++ { 167 | if prevFirstVal.prev == nil { 168 | break 169 | } 170 | removelb++ 171 | prevFirstVal = prevFirstVal.prev 172 | } 173 | 174 | // was able to find previous value 175 | if int64(removelb) == l+1 { 176 | toAdd := math.Max(v.prev.v-v.v, 0) 177 | remval := math.Max(prevFirstVal.v-prevFirstVal.next.v, 0) 178 | newrsiu := prevr.v - remval + toAdd 179 | rsid.Set(v.t, newrsiu) 180 | continue 181 | } 182 | } 183 | } 184 | } 185 | 186 | // previous rsiu does not exist. just keep adding until multiplication is required 187 | fseek++ 188 | if v.prev != nil { 189 | ftot = ftot + math.Max(v.prev.v-v.v, 0) 190 | } 191 | 192 | if fseek == l+1 { 193 | rsid.Set(v.t, ftot) 194 | } 195 | 196 | if v.next == nil { 197 | break 198 | } 199 | if v.t.Equal(stop.t) { 200 | break 201 | } 202 | itervt = v.next.t 203 | } 204 | 205 | return rsid 206 | } 207 | 208 | func getRSI(stop *Value, vs ValueSeries, rsi ValueSeries, l int64) ValueSeries { 209 | 210 | rsiukey := fmt.Sprintf("rsiu:%s:%d", vs.ID(), l) 211 | rsiu := getCache(rsiukey) 212 | if rsiu == nil { 213 | rsiu = NewValueSeries() 214 | } 215 | rsidkey := fmt.Sprintf("rsid:%s:%d", vs.ID(), l) 216 | rsid := getCache(rsidkey) 217 | if rsid == nil { 218 | rsid = NewValueSeries() 219 | } 220 | 221 | rsiu = getRSIU(stop, vs, rsiu, l) 222 | rsid = getRSID(stop, vs, rsid, l) 223 | 224 | rsiu.SetCurrent(stop.t) 225 | rsid.SetCurrent(stop.t) 226 | 227 | setCache(rsiukey, rsiu) 228 | setCache(rsidkey, rsid) 229 | 230 | rs := Div(rsiu, rsid) 231 | rsn := rs.GetFirst() 232 | // set inifinity to 100 233 | for { 234 | if rsn == nil { 235 | break 236 | } 237 | if math.IsInf(rsn.v, 1) { 238 | rs.Set(rsn.t, 100) // set infinity to 100 239 | } 240 | rsn = rsn.next 241 | } 242 | 243 | rmau := RMA(rsiu, l) 244 | rmad := RMA(rsid, l) 245 | 246 | rmadiv := Div(rmau, rmad) 247 | 248 | if rmadiv.GetCurrent() == nil { 249 | return rsi 250 | } 251 | 252 | hundred := ReplaceAll(vs, 100) 253 | 254 | res1 := Div(hundred, AddConst(rmadiv, 1.0)) 255 | res2 := Sub(hundred, res1) 256 | 257 | firstVal := rsi.GetLast() 258 | 259 | if firstVal == nil { 260 | firstVal = vs.GetFirst() 261 | } 262 | 263 | if firstVal == nil { 264 | // if nothing is available, then nothing can be done 265 | return rsi 266 | } 267 | 268 | itervt := firstVal.t 269 | 270 | for { 271 | v := vs.Get(itervt) 272 | v2 := res2.Get(itervt) 273 | if v == nil { 274 | break 275 | } 276 | e := rsi.Get(itervt) 277 | if e != nil && v.next == nil { 278 | break 279 | } 280 | if e != nil { 281 | itervt = v.next.t 282 | continue 283 | } 284 | if v2 != nil { 285 | rsi.Set(v.t, v2.v) 286 | } 287 | 288 | if v.next == nil { 289 | break 290 | } 291 | if v.t.Equal(stop.t) { 292 | break 293 | } 294 | itervt = v.next.t 295 | } 296 | 297 | return rsi 298 | } 299 | -------------------------------------------------------------------------------- /pine/series_rsi_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // TestSeriesRSINoData tests no data scenario 10 | // 11 | // t=time.Time (no iteration) | | 12 | // p=ValueSeries | | 13 | // rsi=ValueSeries | | 14 | func TestSeriesRSINoData(t *testing.T) { 15 | 16 | start := time.Now() 17 | data := OHLCVTestData(start, 4, 5*60*1000) 18 | 19 | series, err := NewOHLCVSeries(data) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | prop := OHLCVAttr(series, OHLCPropClose) 25 | rsi := RSI(prop, 2) 26 | if rsi == nil { 27 | t.Error("Expected to be non nil but got nil") 28 | } 29 | } 30 | 31 | // TestSeriesRSINoIteration tests this sceneario where there's no iteration yet 32 | // 33 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 | 34 | // p=ValueSeries | 14 | 15 | 17 | 18 | 35 | // rsi=ValueSeries | | | | | 36 | func TestSeriesRSINoIteration(t *testing.T) { 37 | 38 | start := time.Now() 39 | data := OHLCVTestData(start, 4, 5*60*1000) 40 | data[0].C = 14 41 | data[1].C = 15 42 | data[2].C = 17 43 | data[3].C = 18 44 | 45 | series, err := NewOHLCVSeries(data) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | prop := OHLCVAttr(series, OHLCPropClose) 51 | rsi := RSI(prop, 2) 52 | if rsi == nil { 53 | t.Error("Expected to be non-nil but got nil") 54 | } 55 | } 56 | 57 | // TestSeriesRSIIteration5 tests this scneario when the iterator is at t=4 is not at the end 58 | // 59 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 (here1) | 5 (here2) | 60 | // p=ValueSeries | 13 | 15 | 11 | 18 | 20 | 61 | // u(close, 2) | nil | nil | 2 | 7 | 9 | 62 | // d(close, 2) | nil | nil | 4 | 4 | 0 | 63 | // rsi(u(close,2), 2) | nil | nil | nil | 4.5 | 6.75 | 64 | // rsi(d(close,2), 2) | nil | nil | nil | 4 | 2 | 65 | // rsi(close, 2) | nil | nil | nil| | 52.9411765 | 77.1428571| 66 | func TestSeriesRSIIteration5(t *testing.T) { 67 | 68 | start := time.Now() 69 | data := OHLCVTestData(start, 5, 5*60*1000) 70 | data[0].C = 13 71 | data[1].C = 15 72 | data[2].C = 11 73 | data[3].C = 18 74 | data[4].C = 20 75 | 76 | series, err := NewOHLCVSeries(data) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | 81 | series.Next() 82 | series.Next() 83 | series.Next() 84 | 85 | testTable := []float64{52.94117647058823, 77.14285714285714} 86 | 87 | for i, v := range testTable { 88 | series.Next() 89 | 90 | prop := OHLCVAttr(series, OHLCPropClose) 91 | rsi := RSI(prop, 2) 92 | if rsi == nil { 93 | t.Errorf("Expected to be non nil but got nil at idx: %d", i) 94 | } 95 | if *rsi.Val() != v { 96 | t.Errorf("Expected %+v but got %+v", v, *rsi.Val()) 97 | } 98 | } 99 | } 100 | 101 | // TestSeriesRSINotEnoughData tests this scneario when the lookback is more than the number of data available 102 | // 103 | // t=time.Time | 1 | 2 | 3 | 4 (here) | 104 | // p=ValueSeries | 14 | 15 | 17 | 18 | 105 | // rsi(close, 5) | nil| nil | nil| nil | 106 | func TestSeriesRSINotEnoughData(t *testing.T) { 107 | 108 | start := time.Now() 109 | data := OHLCVTestData(start, 4, 5*60*1000) 110 | data[0].C = 13 111 | data[1].C = 15 112 | data[2].C = 11 113 | data[3].C = 18 114 | 115 | series, err := NewOHLCVSeries(data) 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | 120 | series.Next() 121 | series.Next() 122 | series.Next() 123 | series.Next() 124 | 125 | testTable := []struct { 126 | lookback int 127 | exp *float64 128 | }{ 129 | { 130 | lookback: 5, 131 | exp: nil, 132 | }, 133 | { 134 | lookback: 6, 135 | exp: nil, 136 | }, 137 | } 138 | 139 | for i, v := range testTable { 140 | prop := OHLCVAttr(series, OHLCPropClose) 141 | 142 | rsi := RSI(prop, int64(v.lookback)) 143 | if rsi == nil { 144 | t.Errorf("Expected to be non nil but got nil at idx: %d", i) 145 | } 146 | if rsi.Val() != v.exp { 147 | t.Errorf("Expected to get %+v but got %+v for lookback %+v", v.exp, *rsi.Val(), v.lookback) 148 | } 149 | } 150 | } 151 | 152 | func TestMemoryLeakRSI(t *testing.T) { 153 | testMemoryLeak(t, func(o OHLCVSeries) error { 154 | prop := OHLCVAttr(o, OHLCPropClose) 155 | RSI(prop, 12) 156 | return nil 157 | }) 158 | } 159 | 160 | func ExampleRSI() { 161 | start := time.Now() 162 | data := OHLCVTestData(start, 10000, 5*60*1000) 163 | series, _ := NewOHLCVSeries(data) 164 | for { 165 | if v, _ := series.Next(); v == nil { 166 | break 167 | } 168 | 169 | close := OHLCVAttr(series, OHLCPropClose) 170 | rsi := RSI(close, 16) 171 | log.Printf("RSI: %+v", rsi.Val()) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /pine/series_sma.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type smaCalcItem struct { 8 | valuetot float64 9 | total int64 10 | seeked int64 11 | } 12 | 13 | // SMA generates a ValueSeries of simple moving averages 14 | func SMA(p ValueSeries, l int64) ValueSeries { 15 | key := fmt.Sprintf("sma:%s:%d", p.ID(), l) 16 | sma := getCache(key) 17 | if sma == nil { 18 | sma = NewValueSeries() 19 | } 20 | if p == nil || p.GetCurrent() == nil { 21 | return sma 22 | } 23 | 24 | // current available value 25 | stop := p.GetCurrent() 26 | // where we left off last time 27 | val := sma.GetLast() 28 | 29 | var f *Value 30 | // if we have not generated any SMAs yet 31 | if val == nil { 32 | f = p.GetFirst() 33 | } else { 34 | 35 | // time has not advanced. return cache 36 | if val.t.Equal(stop.t) { 37 | return sma 38 | } 39 | 40 | // value exists, find where we need to start off 41 | v := p.Get(val.t) 42 | if v == nil { 43 | f = p.GetFirst() 44 | } else { 45 | for i := 0; i < int(l)-1; i++ { 46 | if v.prev == nil { 47 | break 48 | } 49 | v = v.prev 50 | } 51 | f = v 52 | } 53 | } 54 | 55 | // generate from the beginning 56 | calcs := make(map[int64]smaCalcItem) 57 | for { 58 | if f == nil { 59 | break 60 | } 61 | calcs[f.t.Unix()] = smaCalcItem{} 62 | toUpdate := make(map[int64]smaCalcItem) 63 | for k, v := range calcs { 64 | 65 | var nvt float64 66 | if f == nil { 67 | nvt = v.valuetot 68 | } else { 69 | nvt = v.valuetot + f.v 70 | } 71 | toUpdate[k] = smaCalcItem{ 72 | valuetot: nvt, 73 | seeked: v.seeked + 1, 74 | total: v.total + 1, 75 | } 76 | } 77 | 78 | for k := range toUpdate { 79 | calcs[k] = toUpdate[k] 80 | // done seeking lookback times 81 | if toUpdate[k].seeked == l { 82 | if toUpdate[k].total > 0 { 83 | v := toUpdate[k].valuetot / float64(toUpdate[k].total) 84 | sma.Set(f.t, v) 85 | } 86 | delete(calcs, k) 87 | } 88 | } 89 | if f.t.Equal(stop.t) { 90 | break 91 | } 92 | f = f.next 93 | } 94 | 95 | setCache(key, sma) 96 | 97 | sma.SetCurrent(stop.t) 98 | 99 | return sma 100 | } 101 | 102 | var cache map[string]ValueSeries = make(map[string]ValueSeries) 103 | 104 | func getCache(key string) ValueSeries { 105 | return cache[key] 106 | } 107 | 108 | func setCache(key string, v ValueSeries) { 109 | cache[key] = v 110 | } 111 | -------------------------------------------------------------------------------- /pine/series_sma_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // TestSeriesSMANoIteration tests this sceneario where there's no iteration yet 10 | // 11 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 | 12 | // p=ValueSeries | 14 | 15 | 17 | 18 | 13 | // sma=ValueSeries | | | | | 14 | func TestSeriesSMANoIteration(t *testing.T) { 15 | 16 | start := time.Now() 17 | data := OHLCVTestData(start, 4, 5*60*1000) 18 | data[0].C = 14 19 | data[1].C = 15 20 | data[2].C = 17 21 | data[3].C = 18 22 | 23 | series, err := NewOHLCVSeries(data) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | prop := OHLCVAttr(series, OHLCPropClose) 29 | sma := SMA(prop, 2) 30 | if sma.Val() != nil { 31 | t.Errorf("Expected to be nil but got %+v", sma) 32 | } 33 | } 34 | 35 | // TestSeriesSMAIteration3 tests this scneario when the iterator is at t=3 is not at the end 36 | // 37 | // t=time.Time (no iteration) | 1 | 2 | 3 (here) | 4 | 38 | // p=ValueSeries | 13 | 15 | 17 | 18 | 39 | // sma(close, 1) | | | 17 | | 40 | // sma(close, 2) | | | 16 | | 41 | // sma(close, 3) | | | 15 | | 42 | // sma(close, 4) | | | nil | | 43 | func TestSeriesSMAIteration3(t *testing.T) { 44 | 45 | start := time.Now() 46 | data := OHLCVTestData(start, 4, 5*60*1000) 47 | data[0].C = 13 48 | data[1].C = 15 49 | data[2].C = 17 50 | data[3].C = 18 51 | 52 | log.Printf("Data[0].S, %+v, 3s: %+v", data[0].S, data[3].S) 53 | 54 | series, err := NewOHLCVSeries(data) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | series.Next() 60 | series.Next() 61 | series.Next() 62 | 63 | testTable := []struct { 64 | lookback int 65 | exp float64 66 | isNil bool 67 | }{ 68 | { 69 | lookback: 1, 70 | exp: 17, 71 | }, 72 | { 73 | lookback: 2, 74 | exp: 16, 75 | }, 76 | { 77 | lookback: 3, 78 | exp: 15, 79 | }, 80 | { 81 | lookback: 4, 82 | exp: 0, 83 | isNil: true, 84 | }, 85 | } 86 | 87 | for i, v := range testTable { 88 | prop := OHLCVAttr(series, OHLCPropClose) 89 | 90 | sma := SMA(prop, int64(v.lookback)) 91 | 92 | if sma == nil { 93 | t.Errorf("Expected to be non nil but got nil at idx: %d", i) 94 | } 95 | if v.isNil && sma.Val() != nil { 96 | t.Error("expected to be nil but got non nil") 97 | } 98 | if !v.isNil && *sma.Val() != v.exp { 99 | t.Errorf("Expected to get %+v but got %+v for lookback %+v", v.exp, *sma.Val(), v.lookback) 100 | } 101 | } 102 | } 103 | 104 | // TestSeriesSMAIteration4 tests this scneario when the iterator is at t=4 105 | // 106 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 (here) | 107 | // p=ValueSeries | 14 | 15 | 17 | 18 | 108 | // sma(close, 1) | | | | 18 | 109 | // sma(close, 2) | | | | 17.5 | 110 | // sma(close, 3) | | | | 17 | 111 | // sma(close, 4) | | | | 16.5 | 112 | func TestSeriesSMAIteration4(t *testing.T) { 113 | 114 | start := time.Now() 115 | data := OHLCVTestData(start, 4, 5*60*1000) 116 | data[0].C = 15 117 | data[1].C = 16 118 | data[2].C = 17 119 | data[3].C = 18 120 | 121 | log.Printf("Data[0].S, %+v, 3s: %+v", data[0].S, data[3].S) 122 | 123 | series, err := NewOHLCVSeries(data) 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | 128 | series.Next() 129 | series.Next() 130 | series.Next() 131 | series.Next() 132 | 133 | testTable := []struct { 134 | lookback int 135 | exp float64 136 | }{ 137 | { 138 | lookback: 1, 139 | exp: 18, 140 | }, 141 | { 142 | lookback: 2, 143 | exp: 17.5, 144 | }, 145 | { 146 | lookback: 3, 147 | exp: 17, 148 | }, 149 | { 150 | lookback: 4, 151 | exp: 16.5, 152 | }, 153 | } 154 | 155 | for i, v := range testTable { 156 | prop := OHLCVAttr(series, OHLCPropClose) 157 | 158 | sma := SMA(prop, int64(v.lookback)) 159 | if sma == nil { 160 | t.Errorf("Expected to be non nil but got nil at idx: %d", i) 161 | } 162 | if *sma.Val() != v.exp { 163 | t.Errorf("Expected to get %+v but got %+v for lookback %+v", v.exp, sma, v.lookback) 164 | } 165 | } 166 | } 167 | 168 | // TestSeriesSMANested tests nested SMA 169 | // 170 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 (here) | 171 | // p=ValueSeries | 14 | 15 | 17 | 18 | 172 | // sma(close, 2) | | 14.5 | 16 | 17.5 | 173 | // sma(sma(close, 2), 2) | | | 15.25 | 16.75 | 174 | func TestSeriesSMANested(t *testing.T) { 175 | 176 | start := time.Now() 177 | data := OHLCVTestData(start, 4, 5*60*1000) 178 | data[0].C = 14 179 | data[1].C = 15 180 | data[2].C = 17 181 | data[3].C = 18 182 | 183 | log.Printf("Data[0].S, %+v, 3s: %+v", data[0].S, data[3].S) 184 | 185 | series, err := NewOHLCVSeries(data) 186 | if err != nil { 187 | t.Fatal(err) 188 | } 189 | 190 | series.Next() 191 | series.Next() 192 | series.Next() 193 | 194 | testTable := []float64{15.25, 16.75} 195 | 196 | for _, v := range testTable { 197 | prop := OHLCVAttr(series, OHLCPropClose) 198 | 199 | sma := SMA(prop, 2) 200 | sma2 := SMA(sma, int64(2)) 201 | if *sma2.Val() != v { 202 | t.Errorf("expectd %+v but got %+v", v, *sma2.Val()) 203 | } 204 | series.Next() 205 | } 206 | } 207 | 208 | // TestSeriesSMANotEnoughData tests this scneario when the lookback is more than the number of data available 209 | // 210 | // t=time.Time | 1 | 2 | 3 | 4 (here) | 211 | // p=ValueSeries | 14 | 15 | 17 | 18 | 212 | // sma(close, 5) | | | | nil | 213 | func TestSeriesSMANotEnoughData(t *testing.T) { 214 | 215 | start := time.Now() 216 | data := OHLCVTestData(start, 4, 5*60*1000) 217 | data[0].C = 15 218 | data[1].C = 16 219 | data[2].C = 17 220 | data[3].C = 18 221 | 222 | log.Printf("Data[0].S, %+v, 3s: %+v", data[0].S, data[3].S) 223 | 224 | series, err := NewOHLCVSeries(data) 225 | if err != nil { 226 | t.Fatal(err) 227 | } 228 | 229 | series.Next() 230 | series.Next() 231 | series.Next() 232 | series.Next() 233 | 234 | testTable := []struct { 235 | lookback int 236 | exp *float64 237 | }{ 238 | { 239 | lookback: 5, 240 | exp: nil, 241 | }, 242 | { 243 | lookback: 6, 244 | exp: nil, 245 | }, 246 | } 247 | 248 | for i, v := range testTable { 249 | prop := OHLCVAttr(series, OHLCPropClose) 250 | 251 | sma := SMA(prop, int64(v.lookback)) 252 | if sma == nil { 253 | t.Errorf("Expected to be non nil but got nil at idx: %d", i) 254 | } 255 | if sma.Val() != v.exp { 256 | t.Errorf("Expected to get %+v but got %+v for lookback %+v", v.exp, sma, v.lookback) 257 | } 258 | } 259 | } 260 | 261 | func TestMemoryLeakSMA(t *testing.T) { 262 | testMemoryLeak(t, func(o OHLCVSeries) error { 263 | prop := OHLCVAttr(o, OHLCPropClose) 264 | SMA(prop, 12) 265 | return nil 266 | }) 267 | } 268 | 269 | func ExampleSMA() { 270 | start := time.Now() 271 | data := OHLCVTestData(start, 10000, 5*60*1000) 272 | series, _ := NewOHLCVSeries(data) 273 | for { 274 | if v, _ := series.Next(); v == nil { 275 | break 276 | } 277 | 278 | close := OHLCVAttr(series, OHLCPropClose) 279 | sma := SMA(close, 50) 280 | log.Printf("SMA: %+v", sma.Val()) 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /pine/series_stdev.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Stdev generates a ValueSeries of one standard deviation 8 | // 9 | // Simplified formula is 10 | // - s = sqrt(1 / (N-1) * sum(xi - x)^2) 11 | // 12 | // Parameters 13 | // - p - ValueSeries: source data 14 | // - l - int64: lookback periods [1, ∞) 15 | // 16 | // TradingView's PineScript has an option to use an unbiased estimator, however; this function currently supports biased estimator. 17 | // Any effort to add a bias correction factor is welcome. 18 | func Stdev(p ValueSeries, l int64) ValueSeries { 19 | key := fmt.Sprintf("stdev:%s:%d", p.ID(), l) 20 | stdev := getCache(key) 21 | if stdev == nil { 22 | stdev = NewValueSeries() 23 | } 24 | 25 | // current available value 26 | stop := p.GetCurrent() 27 | if stop == nil { 28 | return stdev 29 | } 30 | 31 | if stdev.Get(stop.t) != nil { 32 | return stdev 33 | } 34 | 35 | vari := Variance(p, l) 36 | 37 | stdev = Pow(vari, 0.5) 38 | 39 | setCache(key, stdev) 40 | 41 | stdev.SetCurrent(stop.t) 42 | 43 | return stdev 44 | } 45 | -------------------------------------------------------------------------------- /pine/series_stdev_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // TestSeriesStdevNoData tests no data scenario 11 | // 12 | // t=time.Time (no iteration) | | 13 | // p=ValueSeries | | 14 | // stdev=ValueSeries | | 15 | func TestSeriesStdevNoData(t *testing.T) { 16 | 17 | start := time.Now() 18 | data := OHLCVTestData(start, 4, 5*60*1000) 19 | 20 | series, err := NewOHLCVSeries(data) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | prop := OHLCVAttr(series, OHLCPropClose) 26 | stdev := Stdev(prop, 2) 27 | if stdev == nil { 28 | t.Error("Expected to be non nil but got nil") 29 | } 30 | } 31 | 32 | // TestSeriesStdevNoIteration tests this sceneario where there's no iteration yet 33 | // 34 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 | 35 | // p=ValueSeries | 14 | 15 | 17 | 18 | 36 | // stdev=ValueSeries | | | | | 37 | func TestSeriesStdevNoIteration(t *testing.T) { 38 | 39 | start := time.Now() 40 | data := OHLCVTestData(start, 4, 5*60*1000) 41 | data[0].C = 14 42 | data[1].C = 15 43 | data[2].C = 17 44 | data[3].C = 18 45 | 46 | series, err := NewOHLCVSeries(data) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | prop := OHLCVAttr(series, OHLCPropClose) 52 | stdev := RSI(prop, 2) 53 | if stdev == nil { 54 | t.Error("Expected to be non-nil but got nil") 55 | } 56 | } 57 | 58 | // TestSeriesStdevIteration tests this scneario 59 | // 60 | // t=time.Time | 1 | 2 | 3 | 4 | 5 | 61 | // p=ValueSeries | 13 | 15 | 11 | 19 | 21 | 62 | // sma(p, 3) | nil | nil | 13 | 15 | 17 | 63 | // p - sma(p, 3)(t=1) | nil | nil | nil | nil | nil | 64 | // p - sma(p, 3)(t=2) | nil | nil | nil | nil | nil | 65 | // p - sma(p, 3)(t=3) | 0 | 2 | -2 | 6 | 5 | 66 | // p - sma(p, 3)(t=4) | -2 | 0 | -4 | 4 | 6 | 67 | // p - sma(p, 3)(t=5) | -4 | -2 | -6 | 2 | 4 | 68 | // Stdev(p, 3) | nil | nil | 2 | 4 | 5.2915 | 69 | func TestSeriesStdevIteration(t *testing.T) { 70 | 71 | start := time.Now() 72 | data := OHLCVTestData(start, 5, 5*60*1000) 73 | data[0].C = 13 74 | data[1].C = 15 75 | data[2].C = 11 76 | data[3].C = 19 77 | data[4].C = 21 78 | 79 | series, err := NewOHLCVSeries(data) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | 84 | testTable := []float64{0, 0, 2, 4, 5.2915} 85 | 86 | for i, v := range testTable { 87 | series.Next() 88 | 89 | prop := OHLCVAttr(series, OHLCPropClose) 90 | stdev := Stdev(prop, 3) 91 | exp := v 92 | if exp == 0 { 93 | if stdev.Val() != nil { 94 | t.Fatalf("expected nil but got non nil: %+v testtable item: %d", *stdev.Val(), i) 95 | } 96 | // OK 97 | } 98 | if exp != 0 { 99 | if stdev.Val() == nil { 100 | t.Fatalf("expected non nil: %+v but got nil testtable item: %d", exp, i) 101 | } 102 | if fmt.Sprintf("%.04f", exp) != fmt.Sprintf("%.04f", *stdev.Val()) { 103 | t.Fatalf("expected %+v but got %+v testtable item: %d", exp, *stdev.Val(), i) 104 | } 105 | // OK 106 | } 107 | } 108 | } 109 | 110 | // TestSeriesStdevNotEnoughData tests when the lookback is more than the number of data available 111 | // 112 | // t=time.Time | 1 | 2 | 3 | 4 (here) | 113 | // p=ValueSeries | 14 | 15 | 17 | 18 | 114 | // stdev(close, 5) | nil| nil | nil| nil | 115 | func TestSeriesStdevNotEnoughData(t *testing.T) { 116 | 117 | start := time.Now() 118 | data := OHLCVTestData(start, 4, 5*60*1000) 119 | data[0].C = 13 120 | data[1].C = 15 121 | data[2].C = 11 122 | data[3].C = 18 123 | 124 | series, err := NewOHLCVSeries(data) 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | 129 | series.Next() 130 | series.Next() 131 | series.Next() 132 | series.Next() 133 | 134 | testTable := []struct { 135 | lookback int 136 | exp *float64 137 | }{ 138 | { 139 | lookback: 5, 140 | exp: nil, 141 | }, 142 | { 143 | lookback: 6, 144 | exp: nil, 145 | }, 146 | } 147 | 148 | for i, v := range testTable { 149 | prop := OHLCVAttr(series, OHLCPropClose) 150 | 151 | stdev := Stdev(prop, int64(v.lookback)) 152 | if stdev == nil { 153 | t.Errorf("Expected to be non nil but got nil at idx: %d", i) 154 | } 155 | if stdev.Val() != v.exp { 156 | t.Errorf("Expected to get %+v but got %+v for lookback %+v", v.exp, *stdev.Val(), v.lookback) 157 | } 158 | } 159 | } 160 | 161 | func TestMemoryLeakStdev(t *testing.T) { 162 | testMemoryLeak(t, func(o OHLCVSeries) error { 163 | prop := OHLCVAttr(o, OHLCPropClose) 164 | Stdev(prop, 12) 165 | return nil 166 | }) 167 | } 168 | 169 | func ExampleStdev() { 170 | start := time.Now() 171 | data := OHLCVTestData(start, 10000, 5*60*1000) 172 | series, _ := NewOHLCVSeries(data) 173 | for { 174 | if v, _ := series.Next(); v == nil { 175 | break 176 | } 177 | 178 | close := OHLCVAttr(series, OHLCPropClose) 179 | stdev := Stdev(close, 12) 180 | log.Printf("Stdev: %+v", stdev.Val()) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /pine/series_sum.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Sum generates a ValueSeries of summation of previous values 8 | // 9 | // Parameters 10 | // - p - ValueSeries: source data 11 | // - l - int: lookback periods [1, ∞) 12 | func Sum(p ValueSeries, l int) ValueSeries { 13 | 14 | key := fmt.Sprintf("sum:%s:%d", p.ID(), l) 15 | sum := getCache(key) 16 | if sum == nil { 17 | sum = NewValueSeries() 18 | } 19 | 20 | sum = generateSum(p, sum, l) 21 | 22 | setCache(key, sum) 23 | 24 | return sum 25 | } 26 | 27 | // SumNoCache generates sum without caching 28 | func SumNoCache(p ValueSeries, l int) ValueSeries { 29 | sum := NewValueSeries() 30 | return generateSum(p, sum, l) 31 | } 32 | 33 | func generateSum(p, sum ValueSeries, l int) ValueSeries { 34 | // current available value 35 | stop := p.GetCurrent() 36 | if stop == nil { 37 | return sum 38 | } 39 | sum = getSum(*stop, sum, p, l) 40 | sum.SetCurrent(stop.t) 41 | return sum 42 | } 43 | 44 | func getSum(stop Value, sum ValueSeries, src ValueSeries, l int) ValueSeries { 45 | 46 | // keep track of the source values of sum, maximum of l+1 items 47 | sumSrc := make([]float64, 0) 48 | var startNew *Value 49 | 50 | lastAvail := sum.GetLast() 51 | 52 | if lastAvail == nil { 53 | startNew = src.GetFirst() 54 | } else { 55 | v := src.Get(lastAvail.t) 56 | startNew = v.next 57 | } 58 | 59 | if startNew == nil { 60 | // if nothing is to start with, then nothing can be done 61 | return sum 62 | } 63 | 64 | // populate source values to be summed 65 | if lastAvail != nil { 66 | lastAvailv := src.Get(lastAvail.t) 67 | 68 | for { 69 | if lastAvailv == nil { 70 | break 71 | } 72 | 73 | srcv := src.Get(lastAvailv.t) 74 | // add at the beginning since we go backwards 75 | sumSrc = append([]float64{srcv.v}, sumSrc...) 76 | 77 | if len(sumSrc) == l { 78 | break 79 | } 80 | lastAvailv = lastAvailv.prev 81 | } 82 | } 83 | 84 | // first new time 85 | itervt := startNew.t 86 | 87 | for { 88 | v := src.Get(itervt) 89 | if v == nil { 90 | break 91 | } 92 | 93 | // append new source data 94 | sumSrc = append(sumSrc, v.v) 95 | 96 | var set bool 97 | 98 | // if previous exists, we just subtract from first value and add new value 99 | if v.prev != nil { 100 | e := sum.Get(v.prev.t) 101 | if e != nil && len(sumSrc) == l+1 { 102 | newsum := e.v - sumSrc[0] + v.v 103 | sum.Set(itervt, newsum) 104 | set = true 105 | } 106 | } 107 | 108 | if !set { 109 | if len(sumSrc) >= l { 110 | var ct int 111 | var tot float64 112 | for i := len(sumSrc) - 1; i >= 0; i-- { 113 | ct++ 114 | tot = tot + sumSrc[i] 115 | if ct == l { 116 | break 117 | } 118 | } 119 | sum.Set(itervt, tot) 120 | } 121 | } 122 | 123 | if v.next == nil { 124 | break 125 | } 126 | if v.t.Equal(stop.t) { 127 | break 128 | } 129 | 130 | if len(sumSrc) > l+1 { 131 | sumSrc = sumSrc[1:] 132 | } 133 | itervt = v.next.t 134 | } 135 | 136 | return sum 137 | } 138 | -------------------------------------------------------------------------------- /pine/series_sum_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // TestSeriesSumNoData tests no data scenario 10 | // 11 | // t=time.Time (no iteration) | | 12 | // p=ValueSeries | | 13 | // stdev=ValueSeries | | 14 | func TestSeriesSumNoData(t *testing.T) { 15 | 16 | start := time.Now() 17 | data := OHLCVTestData(start, 4, 5*60*1000) 18 | 19 | series, err := NewOHLCVSeries(data) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | prop := OHLCVAttr(series, OHLCPropClose) 25 | stdev := Sum(prop, 2) 26 | if stdev == nil { 27 | t.Error("Expected to be non nil but got nil") 28 | } 29 | } 30 | 31 | // TestSeriesSumNoIteration tests this sceneario where there's no iteration yet 32 | // 33 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 | 34 | // p=ValueSeries | 14 | 15 | 17 | 18 | 35 | // sum=ValueSeries | | | | | 36 | func TestSeriesSumNoIteration(t *testing.T) { 37 | 38 | start := time.Now() 39 | data := OHLCVTestData(start, 4, 5*60*1000) 40 | data[0].C = 14 41 | data[1].C = 15 42 | data[2].C = 17 43 | data[3].C = 18 44 | 45 | series, err := NewOHLCVSeries(data) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | prop := OHLCVAttr(series, OHLCPropClose) 51 | sum := Sum(prop, 2) 52 | if sum == nil { 53 | t.Error("Expected to be non-nil but got nil") 54 | } 55 | } 56 | 57 | // TestSeriesSumIteration tests this scneario 58 | // 59 | // t=time.Time | 1 | 2 | 3 | 4 | 5 | 60 | // p=ValueSeries | 13 | 15 | 11 | 19 | 21 | 61 | // sum(p, 1) | 13 | 15 | 11 | 19 | 21 | 62 | // sum(p, 2) | nil | 28 | 26 | 30 | 40 | 63 | // sum(p, 3) | nil | nil | 39 | 45 | 51 | 64 | func TestSeriesSumIteration(t *testing.T) { 65 | 66 | start := time.Now() 67 | data := OHLCVTestData(start, 5, 5*60*1000) 68 | data[0].C = 13 69 | data[1].C = 15 70 | data[2].C = 11 71 | data[3].C = 19 72 | data[4].C = 21 73 | 74 | series, err := NewOHLCVSeries(data) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | testTable := []struct { 80 | lookback int 81 | vals []float64 82 | }{ 83 | { 84 | lookback: 1, 85 | vals: []float64{13, 15, 11, 19, 21}, 86 | }, 87 | { 88 | lookback: 2, 89 | vals: []float64{0, 28, 26, 30, 40}, 90 | }, 91 | { 92 | lookback: 3, 93 | vals: []float64{0, 0, 39, 45, 51}, 94 | }, 95 | } 96 | 97 | for j := 0; j <= 3; j++ { 98 | series.Next() 99 | 100 | for i, v := range testTable { 101 | prop := OHLCVAttr(series, OHLCPropClose) 102 | sum := Sum(prop, v.lookback) 103 | exp := v.vals[j] 104 | if exp == 0 { 105 | if sum.Val() != nil { 106 | t.Fatalf("expected nil but got non nil: %+v at vals item: %d, testtable item: %d", *sum.Val(), j, i) 107 | } 108 | // OK 109 | } 110 | if exp != 0 { 111 | if sum.Val() == nil { 112 | t.Fatalf("expected non nil: %+v but got nil at vals item: %d, testtable item: %d", exp, j, i) 113 | } 114 | if exp != *sum.Val() { 115 | t.Fatalf("expected %+v but got %+v at vals item: %d, testtable item: %d", exp, *sum.Val(), j, i) 116 | } 117 | // OK 118 | } 119 | } 120 | } 121 | } 122 | 123 | func TestMemoryLeakSum(t *testing.T) { 124 | testMemoryLeak(t, func(o OHLCVSeries) error { 125 | prop := OHLCVAttr(o, OHLCPropClose) 126 | Sum(prop, 10) 127 | return nil 128 | }) 129 | } 130 | 131 | func ExampleSum() { 132 | start := time.Now() 133 | data := OHLCVTestData(start, 10000, 5*60*1000) 134 | series, _ := NewOHLCVSeries(data) 135 | for { 136 | if v, _ := series.Next(); v == nil { 137 | break 138 | } 139 | 140 | close := OHLCVAttr(series, OHLCPropClose) 141 | // Get the sum of last 10 values 142 | sum := Sum(close, 10) 143 | log.Printf("Sum: %+v", sum.Val()) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /pine/series_value_when.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ValueWhen generates a ValueSeries of float 8 | // arguments are 9 | // - bs: ValueSeries - Value Series where values are 0.0, 1.0 (boolean) 10 | // - src: ValueSeries - Value Series of the source 11 | // - ocr: int - The occurrence of the condition. The numbering starts from 0 and goes back in time, so '0' is the most recent occurrence of `condition`, '1' is the second most recent and so forth. Must be an integer >= 0. 12 | func ValueWhen(bs, src ValueSeries, ocr int) ValueSeries { 13 | key := fmt.Sprintf("valuewhen:%s:%s:%d", bs.ID(), src.ID(), ocr) 14 | vw := getCache(key) 15 | if vw == nil { 16 | vw = NewValueSeries() 17 | } 18 | 19 | // current available value 20 | stop := src.GetCurrent() 21 | 22 | if stop == nil { 23 | return vw 24 | } 25 | 26 | vw = valueWhen(*stop, bs, src, vw, ocr) 27 | 28 | setCache(key, vw) 29 | 30 | vw.SetCurrent(stop.t) 31 | 32 | return vw 33 | } 34 | 35 | func valueWhen(stop Value, bs, src, vw ValueSeries, ocr int) ValueSeries { 36 | 37 | var val *Value 38 | 39 | lastvw := vw.GetCurrent() 40 | if lastvw != nil { 41 | val = bs.Get(lastvw.t) 42 | if val != nil { 43 | val = val.next 44 | } 45 | } else { 46 | val = bs.GetFirst() 47 | } 48 | 49 | if val == nil { 50 | return vw 51 | } 52 | 53 | // populate src values if condition=1.0 54 | condSrc := make([]float64, 0) 55 | 56 | prevVal := val 57 | for { 58 | prevVal = prevVal.prev 59 | if prevVal == nil { 60 | break 61 | } 62 | 63 | b := bs.Get(prevVal.t) 64 | if b == nil { 65 | continue 66 | } 67 | if b.v == 1 { 68 | srcv := src.Get(prevVal.t) 69 | // add at the beginning since we go backwards 70 | condSrc = append([]float64{srcv.v}, condSrc...) 71 | } 72 | 73 | if len(condSrc) == (ocr + 1) { 74 | break 75 | } 76 | } 77 | 78 | // last available does not exist. start from first 79 | 80 | for { 81 | if val == nil { 82 | break 83 | } 84 | // update 85 | if val.v == 1.0 { 86 | srcval := src.Get(val.t) 87 | if srcval != nil { 88 | condSrc = append(condSrc, srcval.v) 89 | if len(condSrc) > (ocr + 1) { 90 | condSrc = condSrc[1:] 91 | } 92 | } 93 | } 94 | 95 | if len(condSrc) == (ocr + 1) { 96 | vwappend := condSrc[0] 97 | vw.Set(val.t, vwappend) 98 | } 99 | 100 | val = val.next 101 | } 102 | 103 | return vw 104 | } 105 | -------------------------------------------------------------------------------- /pine/series_value_when_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // TestSeriesValueWhenNoData tests no data scenario 10 | // 11 | // t=time.Time (no iteration) | | 12 | // p=ValueSeries | | 13 | // valueWhen=ValueSeries | | 14 | func TestSeriesValueWhenNoData(t *testing.T) { 15 | 16 | start := time.Now() 17 | data := OHLCVTestData(start, 0, 5*60*1000) 18 | 19 | series, err := NewOHLCVSeries(data) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | prop := OHLCVAttr(series, OHLCPropClose) 25 | bs := NewValueSeries() 26 | 27 | rsi := ValueWhen(bs, prop, 2) 28 | if rsi == nil { 29 | t.Error("Expected to be non nil but got nil") 30 | } 31 | } 32 | 33 | // TestSeriesValueWhenNoIteration tests this sceneario where there's no iteration yet 34 | // 35 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 | 36 | // p=ValueSeries | 14 | 15 | 17 | 18 | 37 | // bs=ValueSeries |1.0 | 0.0 |1.0 |0.0 | 38 | // valuewhen(0)=ValueSeries | | | | | 39 | func TestSeriesValueWhenNoIteration(t *testing.T) { 40 | 41 | start := time.Now() 42 | data := OHLCVTestData(start, 4, 5*60*1000) 43 | data[0].C = 14 44 | data[1].C = 15 45 | data[2].C = 17 46 | data[3].C = 18 47 | 48 | series, err := NewOHLCVSeries(data) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | bs := NewValueSeries() 54 | bs.Set(data[0].S, 1) 55 | bs.Set(data[1].S, 0) 56 | bs.Set(data[2].S, 1) 57 | bs.Set(data[3].S, 0) 58 | 59 | prop := OHLCVAttr(series, OHLCPropClose) 60 | rsi := ValueWhen(bs, prop, 0) 61 | if rsi == nil { 62 | t.Error("Expected to be non-nil but got nil") 63 | } 64 | } 65 | 66 | // TestSeriesValueWhenIteration5 tests this scneario when the iterator is at t=4 is not at the end 67 | // 68 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 | 5 | 6 | 69 | // bs=ValueSeries | 0 | 1 | 0 | 1 | 1 | 0 | 70 | // src=ValueSeries | 13 | 15 | 11 | 18 | 20 | 17 | 71 | // valuewhen(0)=ValueSeries | nil | 15 | 15 | 18 | 20 | 20 | 72 | // valuewhen(1)=ValueSeries | nil | nil | nil | 15 | 18 | 18 | 73 | // valuewhen(2)=ValueSeries | nil | nil | nil | nil | 15 | 15 | 74 | func TestSeriesValueWhenIteration5(t *testing.T) { 75 | 76 | start := time.Now() 77 | data := OHLCVTestData(start, 6, 5*60*1000) 78 | data[0].C = 13 79 | data[1].C = 15 80 | data[2].C = 11 81 | data[3].C = 18 82 | data[4].C = 20 83 | data[5].C = 17 84 | 85 | bs := NewValueSeries() 86 | bs.Set(data[0].S, 0) 87 | bs.Set(data[1].S, 1) 88 | bs.Set(data[2].S, 0) 89 | bs.Set(data[3].S, 1) 90 | bs.Set(data[4].S, 1) 91 | bs.Set(data[5].S, 0) 92 | 93 | series, err := NewOHLCVSeries(data) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | 98 | testTable := []struct { 99 | ocr int 100 | vals []float64 101 | }{ 102 | { 103 | ocr: 0, 104 | vals: []float64{0, 15, 15, 18, 20, 20}, 105 | }, 106 | { 107 | ocr: 1, 108 | vals: []float64{0, 0, 0, 15, 18, 18}, 109 | }, 110 | { 111 | ocr: 2, 112 | vals: []float64{0, 0, 0, 0, 15, 15}, 113 | }, 114 | } 115 | 116 | for j := 0; j <= 5; j++ { 117 | series.Next() 118 | 119 | for i, v := range testTable { 120 | prop := OHLCVAttr(series, OHLCPropClose) 121 | vw := ValueWhen(bs, prop, v.ocr) 122 | exp := v.vals[j] 123 | if exp == 0 { 124 | if vw.Val() != nil { 125 | t.Fatalf("expected nil but got non nil: %+v at vals item: %d, testtable item: %d", *vw.Val(), j, i) 126 | } 127 | // OK 128 | } 129 | if exp != 0 { 130 | if vw.Val() == nil { 131 | t.Fatalf("expected non nil: %+v but got nil at vals item: %d, testtable item: %d", exp, j, i) 132 | } 133 | if exp != *vw.Val() { 134 | t.Fatalf("expected %+v but got %+v at vals item: %d, testtable item: %d", exp, *vw.Val(), j, i) 135 | } 136 | // OK 137 | } 138 | } 139 | } 140 | } 141 | 142 | // TestSeriesValueWhenNotEnoughData tests this scneario when the lookback is more than the number of data available 143 | // 144 | // t=time.Time | 1 | 2 | 3 | 4 (here) | 145 | // p=ValueSeries | 14 | 15 | 17 | 18 | 146 | // valuewhen(close, 5) | nil| nil | nil| nil | 147 | func TestSeriesValueWhenNotEnoughData(t *testing.T) { 148 | 149 | start := time.Now() 150 | data := OHLCVTestData(start, 4, 5*60*1000) 151 | data[0].C = 13 152 | data[1].C = 15 153 | data[2].C = 11 154 | data[3].C = 18 155 | 156 | series, err := NewOHLCVSeries(data) 157 | if err != nil { 158 | t.Fatal(err) 159 | } 160 | 161 | series.Next() 162 | series.Next() 163 | series.Next() 164 | series.Next() 165 | 166 | bs := NewValueSeries() 167 | bs.Set(data[0].S, 0) 168 | bs.Set(data[1].S, 1) 169 | bs.Set(data[2].S, 0) 170 | bs.Set(data[3].S, 1) 171 | 172 | prop := OHLCVAttr(series, OHLCPropClose) 173 | 174 | vw := ValueWhen(bs, prop, 5) 175 | if vw.Val() != nil { 176 | t.Errorf("Expected nil but got %+v", *vw.Val()) 177 | } 178 | } 179 | 180 | func TestMemoryLeakValueWhen(t *testing.T) { 181 | bs := NewValueSeries() 182 | testMemoryLeak(t, func(o OHLCVSeries) error { 183 | prop := OHLCVAttr(o, OHLCPropClose) 184 | if c := prop.GetCurrent(); c != nil { 185 | bs.Set(c.t, float64(int(c.v)%2)) 186 | } 187 | ValueWhen(bs, prop, 10) 188 | return nil 189 | }) 190 | } 191 | 192 | func BenchmarkValueWhen(b *testing.B) { 193 | // run the Fib function b.N times 194 | start := time.Now() 195 | data := OHLCVTestData(start, 10000, 5*60*1000) 196 | series, _ := NewOHLCVSeries(data) 197 | vals := OHLCVAttr(series, OHLCPropClose) 198 | 199 | bs := NewValueSeries() 200 | for _, v := range data { 201 | bs.Set(v.S, float64(int(v.C)%2)) 202 | } 203 | 204 | for n := 0; n < b.N; n++ { 205 | series.Next() 206 | ValueWhen(bs, vals, 5) 207 | } 208 | } 209 | 210 | func ExampleValueWhen() { 211 | start := time.Now() 212 | data := OHLCVTestData(start, 10000, 5*60*1000) 213 | series, _ := NewOHLCVSeries(data) 214 | 215 | // value series with 0.0 or 1.0 (true/false) 216 | bs := NewValueSeries() 217 | for _, v := range data { 218 | bs.Set(v.S, float64(int(v.C)%2)) 219 | } 220 | 221 | for { 222 | if v, _ := series.Next(); v == nil { 223 | break 224 | } 225 | 226 | close := OHLCVAttr(series, OHLCPropClose) 227 | vw := ValueWhen(close, bs, 12) 228 | log.Printf("ValueWhen: %+v", vw.Val()) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /pine/series_variance.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Variance generates a ValueSeries of variance. 9 | // Variance is the expectation of the squared deviation of a series from its mean (ta.sma, and it informally measures how far a set of numbers are spread out from their mean. 10 | // 11 | // Simplified formula is 12 | // - v = (1 / (N-1) * sum(xi - x)^2) 13 | // 14 | // Parameters 15 | // - p - ValueSeries: source data 16 | // - l - lookback: lookback periods [1, ∞) 17 | // 18 | // TradingView's PineScript has an option to use an unbiased estimator, however; this function currently supports biased estimator. 19 | // Any effort to add a bias correction factor is welcome. 20 | func Variance(p ValueSeries, l int64) ValueSeries { 21 | key := fmt.Sprintf("variance:%s:%d", p.ID(), l) 22 | vari := getCache(key) 23 | if vari == nil { 24 | vari = NewValueSeries() 25 | } 26 | 27 | // current available value 28 | stop := p.GetCurrent() 29 | if stop == nil { 30 | return vari 31 | } 32 | 33 | if vari.Get(stop.t) != nil { 34 | return vari 35 | } 36 | 37 | sma := SMA(p, l) 38 | 39 | meanv := sma.Get(stop.t) 40 | if meanv == nil { 41 | return vari 42 | } 43 | diff := SubConstNoCache(p, meanv.v) 44 | sqrt := Pow(diff, 2) 45 | sum := SumNoCache(sqrt, int(l)) 46 | denom := math.Max(float64(l-1), 1) 47 | vari = DivConstNoCache(sum, denom) 48 | 49 | vari.SetCurrent(stop.t) 50 | 51 | setCache(key, vari) 52 | 53 | return vari 54 | } 55 | -------------------------------------------------------------------------------- /pine/series_variance_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // TestSeriesVarianceNoData tests no data scenario 11 | // 12 | // t=time.Time (no iteration) | | 13 | // p=ValueSeries | | 14 | // variance=ValueSeries | | 15 | func TestSeriesVarianceNoData(t *testing.T) { 16 | 17 | start := time.Now() 18 | data := OHLCVTestData(start, 4, 5*60*1000) 19 | 20 | series, err := NewOHLCVSeries(data) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | prop := OHLCVAttr(series, OHLCPropClose) 26 | variance := Variance(prop, 2) 27 | if variance == nil { 28 | t.Error("Expected to be non nil but got nil") 29 | } 30 | } 31 | 32 | // TestSeriesVarianceNoIteration tests this sceneario where there's no iteration yet 33 | // 34 | // t=time.Time (no iteration) | 1 | 2 | 3 | 4 | 35 | // p=ValueSeries | 14 | 15 | 17 | 18 | 36 | // variance=ValueSeries | | | | | 37 | func TestSeriesVarianceNoIteration(t *testing.T) { 38 | 39 | start := time.Now() 40 | data := OHLCVTestData(start, 4, 5*60*1000) 41 | data[0].C = 14 42 | data[1].C = 15 43 | data[2].C = 17 44 | data[3].C = 18 45 | 46 | series, err := NewOHLCVSeries(data) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | prop := OHLCVAttr(series, OHLCPropClose) 52 | variance := RSI(prop, 2) 53 | if variance == nil { 54 | t.Error("Expected to be non-nil but got nil") 55 | } 56 | } 57 | 58 | // TestSeriesVarianceIteration tests this scneario 59 | // 60 | // t=time.Time | 1 | 2 | 3 | 4 | 5 | 61 | // p=ValueSeries | 13 | 15 | 11 | 19 | 21 | 62 | // sma(p, 3) | nil | nil | 13 | 15 | 17 | 63 | // p - sma(p, 3)(t=1) | nil | nil | nil | nil | nil | 64 | // p - sma(p, 3)(t=2) | nil | nil | nil | nil | nil | 65 | // p - sma(p, 3)(t=3) | 0 | 2 | -2 | 6 | 5 | 66 | // p - sma(p, 3)(t=4) | -2 | 0 | -4 | 4 | 6 | 67 | // p - sma(p, 3)(t=5) | -4 | -2 | -6 | 2 | 4 | 68 | // Variance(p, 3) | nil | nil | 4 | 16 | 28 | 69 | func TestSeriesVarianceIteration(t *testing.T) { 70 | 71 | start := time.Now() 72 | data := OHLCVTestData(start, 5, 5*60*1000) 73 | data[0].C = 13 74 | data[1].C = 15 75 | data[2].C = 11 76 | data[3].C = 19 77 | data[4].C = 21 78 | 79 | series, err := NewOHLCVSeries(data) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | 84 | testTable := []float64{0, 0, 4, 16, 28} 85 | 86 | for i, v := range testTable { 87 | series.Next() 88 | 89 | prop := OHLCVAttr(series, OHLCPropClose) 90 | variance := Variance(prop, 3) 91 | exp := v 92 | if exp == 0 { 93 | if variance.Val() != nil { 94 | t.Fatalf("expected nil but got non nil: %+v testtable item: %d", *variance.Val(), i) 95 | } 96 | // OK 97 | } 98 | if exp != 0 { 99 | if variance.Val() == nil { 100 | t.Fatalf("expected non nil: %+v but got nil testtable item: %d", exp, i) 101 | } 102 | if fmt.Sprintf("%.04f", exp) != fmt.Sprintf("%.04f", *variance.Val()) { 103 | t.Fatalf("expected %+v but got %+v testtable item: %d", exp, *variance.Val(), i) 104 | } 105 | // OK 106 | } 107 | } 108 | } 109 | 110 | // TestSeriesVarianceNotEnoughData tests when the lookback is more than the number of data available 111 | // 112 | // t=time.Time | 1 | 2 | 3 | 4 (here) | 113 | // p=ValueSeries | 14 | 15 | 17 | 18 | 114 | // variance(close, 5) | nil| nil | nil| nil | 115 | func TestSeriesVarianceNotEnoughData(t *testing.T) { 116 | 117 | start := time.Now() 118 | data := OHLCVTestData(start, 4, 5*60*1000) 119 | data[0].C = 13 120 | data[1].C = 15 121 | data[2].C = 11 122 | data[3].C = 18 123 | 124 | series, err := NewOHLCVSeries(data) 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | 129 | series.Next() 130 | series.Next() 131 | series.Next() 132 | series.Next() 133 | 134 | testTable := []struct { 135 | lookback int 136 | exp *float64 137 | }{ 138 | { 139 | lookback: 5, 140 | exp: nil, 141 | }, 142 | { 143 | lookback: 6, 144 | exp: nil, 145 | }, 146 | } 147 | 148 | for i, v := range testTable { 149 | prop := OHLCVAttr(series, OHLCPropClose) 150 | 151 | variance := Variance(prop, int64(v.lookback)) 152 | if variance == nil { 153 | t.Errorf("Expected to be non nil but got nil at idx: %d", i) 154 | } 155 | if variance.Val() != v.exp { 156 | t.Errorf("Expected to get %+v but got %+v for lookback %+v", v.exp, *variance.Val(), v.lookback) 157 | } 158 | } 159 | } 160 | 161 | func TestMemoryLeakVariance(t *testing.T) { 162 | testMemoryLeak(t, func(o OHLCVSeries) error { 163 | prop := OHLCVAttr(o, OHLCPropClose) 164 | Variance(prop, 10) 165 | return nil 166 | }) 167 | } 168 | 169 | func BenchmarkVariance(b *testing.B) { 170 | // run the Fib function b.N times 171 | start := time.Now() 172 | data := OHLCVTestData(start, 10000, 5*60*1000) 173 | series, _ := NewOHLCVSeries(data) 174 | vals := OHLCVAttr(series, OHLCPropClose) 175 | 176 | for n := 0; n < b.N; n++ { 177 | series.Next() 178 | Variance(vals, 5) 179 | } 180 | } 181 | 182 | func ExampleVariance() { 183 | start := time.Now() 184 | data := OHLCVTestData(start, 10000, 5*60*1000) 185 | series, _ := NewOHLCVSeries(data) 186 | for { 187 | if v, _ := series.Next(); v == nil { 188 | break 189 | } 190 | 191 | close := OHLCVAttr(series, OHLCPropClose) 192 | variance := Variance(close, 20) 193 | log.Printf("Variance: %+v", variance.Val()) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /pine/testdata_ohlcv_no_gap.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | // OHLCVTestData generates test data 10 | func OHLCVTestData(start time.Time, period, intervalms int64) []OHLCV { 11 | s := start 12 | v := make([]OHLCV, 0) 13 | for i := 0; i < int(period); i++ { 14 | ohlcv := generateOHLCV(s) 15 | ohlcv.S = s 16 | v = append(v, ohlcv) 17 | s = s.Add(time.Duration(intervalms * 1e6)) 18 | } 19 | return v 20 | } 21 | 22 | func OHLCVStaticTestData() []OHLCV { 23 | start := time.Now() 24 | data := []OHLCV{ 25 | OHLCV{O: 11.3, H: 19.7, L: 11.1, C: 16.5, V: 11.6}, 26 | OHLCV{O: 12.9, H: 19.1, L: 12.3, C: 18.7, V: 13.0}, 27 | OHLCV{O: 11.0, H: 18.8, L: 10.3, C: 18.2, V: 13.8}, 28 | OHLCV{O: 19.2, H: 19.6, L: 11.7, C: 11.9, V: 15.9}, 29 | OHLCV{O: 18.1, H: 19.5, L: 11.2, C: 19.3, V: 16.8}, 30 | OHLCV{O: 19.4, H: 19.8, L: 13.5, C: 14.2, V: 19.1}, 31 | OHLCV{O: 19.1, H: 19.5, L: 12.9, C: 14.4, V: 14.7}, 32 | OHLCV{O: 10.6, H: 19.9, L: 10.3, C: 11.0, V: 11.7}, 33 | OHLCV{O: 18.8, H: 19.0, L: 12.4, C: 14.7, V: 17.4}, 34 | OHLCV{O: 17.1, H: 17.6, L: 10.0, C: 10.3, V: 15.0}, 35 | } 36 | 37 | for i := range data { 38 | fivemin := 5 * time.Minute 39 | data[i].S = start.Add(time.Duration(i) * fivemin) 40 | } 41 | return data 42 | } 43 | 44 | func generateOHLCV(t time.Time) OHLCV { 45 | max := 20.0 46 | min := 10.0 47 | 48 | o := randVal(min, max) 49 | c := randVal(min, max) 50 | vol := randVal(min, max) 51 | v := OHLCV{ 52 | O: o, 53 | C: c, 54 | V: vol, 55 | } 56 | 57 | h2 := math.Max(c, o) 58 | h := randVal(h2, max) 59 | v.H = h 60 | 61 | l2 := math.Min(c, o) 62 | l := randVal(min, l2) 63 | v.L = l 64 | 65 | return v 66 | } 67 | 68 | func randVal(min, max float64) float64 { 69 | return rand.Float64()*(max-min) + min 70 | } 71 | -------------------------------------------------------------------------------- /pine/value_series.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/twinj/uuid" 8 | ) 9 | 10 | type ValueSeries interface { 11 | ID() string 12 | 13 | // Get gets the item by time in value series 14 | Get(time.Time) *Value 15 | // GetLast gets the last item in value series 16 | GetLast() *Value 17 | // GetFirst gets the first item in value series 18 | GetFirst() *Value 19 | 20 | // Gets size of the ValueSeries 21 | Len() int 22 | 23 | Set(time.Time, float64) 24 | 25 | Shift() bool 26 | 27 | Val() *float64 28 | SetCurrent(time.Time) bool 29 | GetCurrent() *Value 30 | 31 | // set the maximum number of items. 32 | // This helps prevent allocating too much memory 33 | SetMax(int64) 34 | } 35 | 36 | type valueSeries struct { 37 | id string 38 | cur *Value 39 | first *Value 40 | last *Value 41 | // max number of candles. 0 means no limit. Defaults to 1000 42 | max int64 43 | sync.Mutex 44 | timemap map[int64]*Value 45 | } 46 | 47 | type Value struct { 48 | t time.Time 49 | v float64 50 | prev *Value 51 | next *Value 52 | } 53 | 54 | // NewValueSeries creates an empty series that conforms to ValueSeries 55 | func NewValueSeries() ValueSeries { 56 | u := uuid.NewV4() 57 | v := &valueSeries{ 58 | id: u.String(), 59 | max: 1000, // default maximum items 60 | timemap: make(map[int64]*Value), 61 | } 62 | return v 63 | } 64 | 65 | func (s *valueSeries) Len() int { 66 | return len(s.timemap) 67 | } 68 | 69 | func (s *valueSeries) SetMax(m int64) { 70 | s.max = m 71 | s.resize() 72 | } 73 | 74 | func (s *valueSeries) ID() string { 75 | return s.id 76 | } 77 | 78 | func (s *valueSeries) SetCurrent(t time.Time) bool { 79 | v, ok := s.timemap[t.Unix()] 80 | if !ok { 81 | s.cur = nil 82 | return false 83 | } 84 | s.cur = v 85 | return true 86 | } 87 | 88 | func (s *valueSeries) GetCurrent() *Value { 89 | return s.cur 90 | } 91 | 92 | func (s *valueSeries) GetFirst() *Value { 93 | return s.first 94 | } 95 | 96 | func (s *valueSeries) GetLast() *Value { 97 | return s.last 98 | } 99 | 100 | func (s *valueSeries) Val() *float64 { 101 | if s.cur == nil { 102 | return nil 103 | } 104 | return &s.cur.v 105 | } 106 | 107 | func (s *valueSeries) Get(t time.Time) *Value { 108 | return s.getValue(t.Unix()) 109 | } 110 | 111 | func (s *valueSeries) getValue(t int64) *Value { 112 | return s.timemap[t] 113 | } 114 | 115 | func (s *valueSeries) setValue(t int64, v *Value) { 116 | s.timemap[t] = v 117 | s.resize() 118 | } 119 | 120 | // Set appends to the end of the series. If same timestamp exists, its value will be replaced 121 | func (s *valueSeries) Set(t time.Time, val float64) { 122 | curval := s.getValue(t.Unix()) 123 | if curval != nil { 124 | // replace existing 125 | v2 := &Value{ 126 | next: curval.next, 127 | prev: curval.prev, 128 | t: t, 129 | v: val, 130 | } 131 | if curval.prev != nil { 132 | curval.prev.next = v2 133 | } 134 | if curval.next != nil { 135 | curval.next.prev = v2 136 | } 137 | if s.cur == curval { 138 | s.cur = v2 139 | } 140 | if s.first == curval { 141 | s.first = v2 142 | } 143 | if s.last == curval { 144 | s.last = v2 145 | } 146 | s.setValue(t.Unix(), v2) 147 | return 148 | } 149 | 150 | v := &Value{ 151 | t: t, 152 | v: val, 153 | } 154 | if s.last != nil { 155 | s.last.next = v 156 | v.prev = s.last 157 | } 158 | s.last = v 159 | if s.first == nil { 160 | s.first = v 161 | } 162 | s.setValue(t.Unix(), v) 163 | } 164 | 165 | func (s *valueSeries) resize() { 166 | // set to unlimited, nothing to perform 167 | if s.max == 0 { 168 | return 169 | } 170 | for { 171 | if int64(s.Len()) <= s.max { 172 | break 173 | } 174 | s.Shift() 175 | } 176 | } 177 | 178 | func (s *valueSeries) Shift() bool { 179 | if s.first == nil { 180 | return false 181 | } 182 | delete(s.timemap, s.first.t.Unix()) 183 | s.first = s.first.next 184 | if s.first != nil { 185 | s.first.prev = nil 186 | } 187 | return true 188 | } 189 | -------------------------------------------------------------------------------- /pine/value_series_test.go: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestValueSeriesAdd(t *testing.T) { 10 | a := NewValueSeries() 11 | now := time.Now() 12 | a.Set(now, 1) 13 | a.Set(now.Add(time.Duration(1000*1e6)), 2) 14 | a.Set(now.Add(time.Duration(2000*1e6)), 4) // this doesn't exist in b 15 | 16 | b := NewValueSeries() 17 | b.Set(now, 4) 18 | b.Set(now.Add(time.Duration(1000*1e6)), 4) 19 | 20 | c := Add(a, b) 21 | c.SetCurrent(now) 22 | f := c.GetCurrent() 23 | if f == nil { 24 | t.Fatalf("expected to be non nil but got nil") 25 | } 26 | if f.v != 5 { 27 | t.Errorf("expected %+v but got %+v", 5, f.v) 28 | } 29 | if f.next.v != 6 { 30 | t.Errorf("expected %+v but got %+v", 6, f.v) 31 | } 32 | if f.next.next != nil { 33 | t.Errorf("expected nil but got %+v", f.next.next.v) 34 | } 35 | 36 | // current time is passed on 37 | a.SetCurrent(now.Add(time.Duration(1000 * 1e6))) 38 | d := Add(a, b) 39 | if *d.Val() != 6 { 40 | t.Errorf("expected 6 but got %+v", *d.Val()) 41 | } 42 | } 43 | 44 | func TestValueSeriesAddConst(t *testing.T) { 45 | a := NewValueSeries() 46 | now := time.Now() 47 | t2 := now.Add(time.Duration(1000 * 1e6)) 48 | a.Set(now, 1) 49 | a.Set(t2, 2) 50 | 51 | b := AddConst(a, 3) 52 | f := b.GetFirst() 53 | if f == nil { 54 | t.Fatalf("expected to be non nil but got nil") 55 | } 56 | if f.v != 4 { 57 | t.Errorf("expected %+v but got %+v", 4, f.v) 58 | } 59 | if f.next.v != 5 { 60 | t.Errorf("expected %+v but got %+v", 5, f.v) 61 | } 62 | 63 | // current time is passed on 64 | a.SetCurrent(t2) 65 | d := AddConst(a, 3) 66 | if *d.Val() != 5 { 67 | t.Errorf("expected 5 but got %+v", *d.Val()) 68 | } 69 | } 70 | 71 | func TestValueSeriesOperator(t *testing.T) { 72 | a := NewValueSeries() 73 | now := time.Now() 74 | a.Set(now, 1) 75 | a.Set(now.Add(time.Duration(1000*1e6)), 2) 76 | a.Set(now.Add(time.Duration(2000*1e6)), 3) 77 | 78 | c := Operate(a, a, "testvalueseriesoperator", func(b, c float64) float64 { 79 | return math.Mod(b, 2) 80 | }) 81 | 82 | f := c.GetFirst() 83 | if f == nil { 84 | t.Fatalf("expected to be non nil but got nil") 85 | } 86 | if f.v != 1 { 87 | t.Errorf("expected %+v but got %+v", 0, f.v) 88 | } 89 | if f.next.v != 0 { 90 | t.Errorf("expected %+v but got %+v", 0, f.next.v) 91 | } 92 | if f.next.next.v != 1 { 93 | t.Errorf("expected %+v but got %+v", 1, f.next.next.v) 94 | } 95 | } 96 | 97 | func TestValueSeriesOperatorWithNil(t *testing.T) { 98 | a := NewValueSeries() 99 | b := NewValueSeries() 100 | t1 := time.Now() 101 | t2 := t1.Add(time.Duration(1000 * 1e6)) 102 | t3 := t2.Add(time.Duration(1000 * 1e6)) 103 | a.Set(t1, 1) 104 | 105 | b.Set(t1, 1) 106 | b.Set(t2, 2) 107 | b.Set(t3, 3) 108 | 109 | c := OperateWithNil(b, a, "testoperatewithnil", func(bvalue, avalue *Value) *Value { 110 | if avalue == nil { 111 | return &Value{ 112 | t: bvalue.t, 113 | v: 0, 114 | } 115 | } 116 | return &Value{ 117 | t: avalue.t, 118 | v: avalue.v + bvalue.v, 119 | } 120 | }) 121 | 122 | f := c.GetFirst() 123 | if f == nil { 124 | t.Fatalf("expected to be non nil but got nil") 125 | } 126 | 127 | if f.v != 2 { 128 | t.Errorf("expected %+v but got %+v", 2, f.v) 129 | } 130 | if f.next.v != 0 { 131 | t.Errorf("expected %+v but got %+v", 0, f.next.v) 132 | } 133 | if f.next.next.v != 0 { 134 | t.Errorf("expected %+v but got %+v", 0, f.next.next.v) 135 | } 136 | } 137 | 138 | func TestValueSeriesDiv(t *testing.T) { 139 | a := NewValueSeries() 140 | now := time.Now() 141 | a.Set(now, 1) 142 | a.Set(now.Add(time.Duration(1000*1e6)), 2) 143 | a.Set(now.Add(time.Duration(2000*1e6)), 3) 144 | 145 | b := NewValueSeries() 146 | b.Set(now, 4) 147 | b.Set(now.Add(time.Duration(1000*1e6)), 4) 148 | 149 | c := Div(a, b) 150 | c.SetCurrent(now) 151 | f := c.GetCurrent() 152 | if f == nil { 153 | t.Fatalf("expected to be non nil but got nil") 154 | } 155 | if f.v != 0.25 { 156 | t.Errorf("expected %+v but got %+v", 0.25, f.v) 157 | } 158 | if f.next.v != 0.5 { 159 | t.Errorf("expected %+v but got %+v", 0.5, f.v) 160 | } 161 | if f.next.next != nil { 162 | t.Errorf("expected nil but got %+v", f.next.next.v) 163 | } 164 | 165 | // current time is passed on 166 | a.SetCurrent(now.Add(time.Duration(1000 * 1e6))) 167 | d := Div(a, b) 168 | if *d.Val() != 0.5 { 169 | t.Errorf("expected .5 but got %+v", *d.Val()) 170 | } 171 | } 172 | 173 | func TestValueSeriesDivConst(t *testing.T) { 174 | a := NewValueSeries() 175 | now := time.Now() 176 | a.Set(now, 1) 177 | a.Set(now.Add(time.Duration(1000*1e6)), 2) 178 | 179 | b := DivConst(a, 4) 180 | f := b.GetFirst() 181 | if f == nil { 182 | t.Fatalf("expected to be non nil but got nil") 183 | } 184 | if f.v != 0.25 { 185 | t.Errorf("expected %+v but got %+v", 0.25, f.v) 186 | } 187 | if f.next.v != 0.5 { 188 | t.Errorf("expected %+v but got %+v", 0.5, f.v) 189 | } 190 | 191 | // current time is passed on 192 | a.SetCurrent(now.Add(time.Duration(1000 * 1e6))) 193 | d := DivConst(a, 4) 194 | if *d.Val() != 0.5 { 195 | t.Errorf("expected .5 but got %+v", *d.Val()) 196 | } 197 | } 198 | 199 | // TestValueSeriesSetMaxResize tests when set max is called after data is populated 200 | func TestValueSeriesSetMaxResize(t *testing.T) { 201 | a := NewValueSeries() 202 | now := time.Now() 203 | t1 := time.Now() 204 | t2 := now.Add(time.Duration(1000 * 1e6)) 205 | t3 := now.Add(time.Duration(2000 * 1e6)) 206 | a.Set(t1, 1) 207 | a.Set(t2, 2) 208 | a.Set(t3, 4) // this doesn't exist in b 209 | a.SetMax(2) 210 | 211 | v1 := a.Get(t1) 212 | if v1 != nil { 213 | t.Errorf("expected to be nil but got %+v", v1.v) 214 | } 215 | v1 = a.Get(t2) 216 | if v1.v != 2 { 217 | t.Errorf("expected to be 2 but got %+v", v1.v) 218 | } 219 | v1 = a.Get(t3) 220 | if v1.v != 4 { 221 | t.Errorf("expected to be 4 but got %+v", v1.v) 222 | } 223 | if a.Len() != 2 { 224 | t.Errorf("expected to be 2 but got %+v", a.Len()) 225 | } 226 | } 227 | 228 | // TestValueSeriesSetMaxPushResize tests when max is set and then data is populated 229 | func TestValueSeriesSetMaxPushResize(t *testing.T) { 230 | a := NewValueSeries() 231 | a.SetMax(2) 232 | now := time.Now() 233 | t1 := time.Now() 234 | t2 := now.Add(time.Duration(1000 * 1e6)) 235 | t3 := now.Add(time.Duration(2000 * 1e6)) 236 | a.Set(t1, 1) 237 | a.Set(t2, 2) 238 | a.Set(t3, 4) // this doesn't exist in b 239 | 240 | v1 := a.Get(t1) 241 | if v1 != nil { 242 | t.Errorf("expected to be nil but got %+v", v1.v) 243 | } 244 | v1 = a.Get(t2) 245 | if v1.v != 2 { 246 | t.Errorf("expected to be 2 but got %+v", v1.v) 247 | } 248 | v1 = a.Get(t3) 249 | if v1.v != 4 { 250 | t.Errorf("expected to be 4 but got %+v", v1.v) 251 | } 252 | if a.Len() != 2 { 253 | t.Errorf("expected to be 2 but got %+v", a.Len()) 254 | } 255 | } 256 | 257 | func TestValueSeriesMul(t *testing.T) { 258 | a := NewValueSeries() 259 | now := time.Now() 260 | a.Set(now, 1) 261 | a.Set(now.Add(time.Duration(1000*1e6)), 2) 262 | a.Set(now.Add(time.Duration(2000*1e6)), 3) // this doesn't exist in b 263 | 264 | b := NewValueSeries() 265 | b.Set(now, 4) 266 | b.Set(now.Add(time.Duration(1000*1e6)), 4) 267 | 268 | c := Mul(a, b) 269 | c.SetCurrent(now) 270 | f := c.GetCurrent() 271 | if f == nil { 272 | t.Fatalf("expected to be non nil but got nil") 273 | } 274 | if f.v != 4 { 275 | t.Errorf("expected %+v but got %+v", 4, f.v) 276 | } 277 | if f.next.v != 8 { 278 | t.Errorf("expected %+v but got %+v", 8, f.v) 279 | } 280 | if f.next.next != nil { 281 | t.Errorf("expected nil but got %+v", f.next.next.v) 282 | } 283 | 284 | // current time is passed on 285 | a.SetCurrent(now.Add(time.Duration(1000 * 1e6))) 286 | d := Mul(a, b) 287 | if *d.Val() != 8 { 288 | t.Errorf("expected 8 but got %+v", *d.Val()) 289 | } 290 | } 291 | 292 | func TestValueSeriesMulConst(t *testing.T) { 293 | a := NewValueSeries() 294 | now := time.Now() 295 | a.Set(now, 1) 296 | a.Set(now.Add(time.Duration(1000*1e6)), 2) 297 | 298 | b := MulConst(a, 3) 299 | f := b.GetFirst() 300 | if f == nil { 301 | t.Fatalf("expected to be non nil but got nil") 302 | } 303 | if f.v != 3 { 304 | t.Errorf("expected %+v but got %+v", 3, f.v) 305 | } 306 | if f.next.v != 6 { 307 | t.Errorf("expected %+v but got %+v", 6, f.v) 308 | } 309 | 310 | // current time is passed on 311 | a.SetCurrent(now.Add(time.Duration(1000 * 1e6))) 312 | d := MulConst(a, 3) 313 | if *d.Val() != 6.0 { 314 | t.Errorf("expected 6 but got %+v", *d.Val()) 315 | } 316 | } 317 | 318 | func TestValueSeriesSub(t *testing.T) { 319 | a := NewValueSeries() 320 | now := time.Now() 321 | nilTime := now.Add(time.Duration(3000 * 1e6)) 322 | a.Set(now, 1) 323 | a.Set(now.Add(time.Duration(1000*1e6)), 2) 324 | a.Set(now.Add(time.Duration(2000*1e6)), 3) 325 | a.Set(nilTime, 4) 326 | 327 | b := NewValueSeries() 328 | b.Set(now, 4) 329 | b.Set(now.Add(time.Duration(1000*1e6)), 4) 330 | b.Set(now.Add(time.Duration(2000*1e6)), 1) 331 | 332 | c := Sub(a, b) 333 | c.SetCurrent(now) 334 | f := c.GetCurrent() 335 | if f == nil { 336 | t.Fatalf("expected to be non nil but got nil") 337 | } 338 | if f.v != -3 { 339 | t.Errorf("expected %+v but got %+v", -3, f.v) 340 | } 341 | if f.next.v != -2 { 342 | t.Errorf("expected %+v but got %+v", -2, f.next.v) 343 | } 344 | if f.next.next.v != 2 { 345 | t.Errorf("expected %+v but got %+v", 2, f.next.next.v) 346 | } 347 | n := c.Get(nilTime) 348 | if n != nil { 349 | t.Errorf("expected nil but got %+v", n.v) 350 | } 351 | 352 | // current time is passed on 353 | a.SetCurrent(now.Add(time.Duration(1000 * 1e6))) 354 | d := Sub(a, b) 355 | if *d.Val() != -2 { 356 | t.Errorf("expected -2 but got %+v", *d.Val()) 357 | } 358 | } 359 | 360 | func TestValueSeriesSubConst(t *testing.T) { 361 | a := NewValueSeries() 362 | now := time.Now() 363 | a.Set(now, 1) 364 | a.Set(now.Add(time.Duration(1000*1e6)), 2) 365 | 366 | b := SubConst(a, 3) 367 | f := b.GetFirst() 368 | if f == nil { 369 | t.Fatalf("expected to be non nil but got nil") 370 | } 371 | if f.v != -2 { 372 | t.Errorf("expected %+v but got %+v", -2, f.v) 373 | } 374 | if f.next.v != -1 { 375 | t.Errorf("expected %+v but got %+v", -1, f.v) 376 | } 377 | 378 | // current time is passed on 379 | a.SetCurrent(now.Add(time.Duration(1000 * 1e6))) 380 | d := SubConst(a, 3) 381 | if *d.Val() != -1 { 382 | t.Errorf("expected -1 but got %+v", *d.Val()) 383 | } 384 | } 385 | 386 | func TestValueSeriesGetFirst(t *testing.T) { 387 | 388 | s := NewValueSeries() 389 | now := time.Now() 390 | s.Set(now, 1) 391 | s.Set(now.Add(time.Duration(1000*1e6)), 2) 392 | s.SetCurrent(now) 393 | f := s.GetFirst() 394 | if f == nil { 395 | t.Errorf("expected to be non nil but got nil") 396 | } 397 | if f.next == nil { 398 | t.Errorf("expected next to be non nil but got nil") 399 | } 400 | if f.next.v != 2 { 401 | t.Errorf("expected next value to be 2 but got %+v", f.next.v) 402 | } 403 | } 404 | 405 | func TestMemoryLeakArithmetic(t *testing.T) { 406 | v := 4.2351 407 | 408 | testMemoryLeak(t, func(o OHLCVSeries) error { 409 | c := OHLCVAttr(o, OHLCPropClose) 410 | op := OHLCVAttr(o, OHLCPropOpen) 411 | s1 := Add(c, op) 412 | s2 := AddConst(s1, v) 413 | s3 := Sub(s2, c) 414 | s4 := SubConst(s3, v) 415 | s5 := Mul(s4, s2) 416 | s6 := MulConst(s5, v) 417 | s7 := Div(s6, c) 418 | DivConst(s7, v) 419 | 420 | return nil 421 | }) 422 | } 423 | -------------------------------------------------------------------------------- /testutil/generate_time.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import "time" 4 | 5 | func GenerateTime() time.Time { 6 | return time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) 7 | } 8 | --------------------------------------------------------------------------------