├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── lib ├── atr.ex ├── ma.ex ├── macd.ex ├── rsi.ex └── util.ex ├── mix.exs ├── mix.lock └── test ├── atr_test.exs ├── ma_test.exs ├── macd_test.exs ├── rsi_test.exs ├── support └── fixtures.ex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | import_deps: [:typed_struct] 5 | ] 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | trade_indicators-*.tar 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rob Christian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trade Indicators 2 | 3 | Feedback and contributions are welcome. 4 | 5 | Unit tests are passing. Please refer to the tests to see how these are used. 6 | 7 | Each indicator is a state machine. Use the respective module struct for initial 8 | state. OHLCV chart data is a list and each new bar is prepended before passing it 9 | into the indicator `step/2` function. The indicator must be run once each time a 10 | new bar is prepended or updated. In the tests, you can see how to use 11 | `Enum.reduce/3` to run the indicator on your chart data. 12 | 13 | 14 | ## Installation 15 | 16 | Not currently available in Hex. Please reference this repo to install: 17 | 18 | ```elixir 19 | def deps do 20 | [ 21 | { 22 | :trade_indicators, 23 | git: "https://github.com/ElixirTradingTools/trade_indicators.git", 24 | ref: "" 25 | } 26 | ] 27 | end 28 | ``` 29 | -------------------------------------------------------------------------------- /lib/atr.ex: -------------------------------------------------------------------------------- 1 | defmodule TradeIndicators.ATR do 2 | use TypedStruct 3 | alias __MODULE__, as: ATR 4 | alias __MODULE__.Item 5 | alias TradeIndicators.MA 6 | alias TradeIndicators.Util, as: U 7 | alias Decimal, as: D 8 | alias Enum, as: E 9 | alias Map, as: M 10 | 11 | typedstruct do 12 | field :list, List.t(), default: [] 13 | field :period, pos_integer(), default: 14 14 | field :method, :ema | :wma, default: :ema 15 | end 16 | 17 | typedstruct module: Item do 18 | field :avg, D.t() | nil, default: nil 19 | field :tr, D.t() | nil, default: nil 20 | field :t, non_neg_integer() 21 | end 22 | 23 | @zero D.new(0) 24 | 25 | def step(chart = %ATR{list: atr_list, period: period, method: method}, bars) 26 | when is_list(bars) and is_list(atr_list) and is_integer(period) and period > 1 do 27 | ts = 28 | case bars do 29 | [%{t: t} | _] -> t 30 | _ -> nil 31 | end 32 | 33 | case {bars, atr_list} do 34 | {[], _} -> 35 | chart 36 | 37 | {[%{t: t1} | bars_tail], [%{t: t2} | atr_tail]} when t1 == t2 -> 38 | if length(bars) < period do 39 | new_atr = %{avg: nil, t: ts, tr: E.take(bars_tail, 2) |> get_tr()} 40 | %{chart | list: [new_atr | atr_tail]} 41 | else 42 | new_atr = E.take(bars_tail, 2) |> get_tr() |> get_atr(atr_list, period, ts, method) 43 | %{chart | list: [new_atr | tl(atr_list)]} 44 | end 45 | 46 | _ -> 47 | if length(bars) < period do 48 | new_atr = %{avg: nil, t: ts, tr: E.take(bars, 2) |> get_tr()} 49 | %{chart | list: [new_atr | atr_list]} 50 | else 51 | new_atr = E.take(bars, 2) |> get_tr() |> get_atr(atr_list, period, ts, method) 52 | %{chart | list: [new_atr | atr_list]} 53 | end 54 | end 55 | end 56 | 57 | def get_tr([]), do: @zero 58 | def get_tr([%{c: c, h: h, l: l}]), do: get_tr(c, h, l) 59 | def get_tr([%{h: h, l: l}, %{c: c}]), do: get_tr(c, h, l) 60 | 61 | def get_tr(c, h, l) 62 | when (is_binary(c) or is_integer(c)) and 63 | (is_binary(h) or is_integer(h)) and 64 | (is_binary(l) or is_integer(l)), 65 | do: get_tr(D.new(c), D.new(h), D.new(l)) 66 | 67 | def get_tr(c = %D{}, h = %D{}, l = %D{}) do 68 | D.sub(h, l) 69 | |> D.max(D.abs(D.sub(h, c))) 70 | |> D.max(D.abs(D.sub(l, c))) 71 | end 72 | 73 | def make_tr_list(new_tr, atr_list, period) do 74 | atr_list 75 | |> E.take(period - 1) 76 | |> E.map(fn %{tr: v} -> v || @zero end) 77 | |> case do 78 | list -> [new_tr | list] 79 | end 80 | end 81 | 82 | def get_atr(new_tr, atr_list, period, ts, avg_fn) when avg_fn in [:wma, :ema] do 83 | %Item{ 84 | avg: get_avg(atr_list, new_tr, period, avg_fn), 85 | tr: new_tr, 86 | t: ts 87 | } 88 | end 89 | 90 | def get_avg(atr_list, new_tr, period, :wma) do 91 | new_tr 92 | |> make_tr_list(atr_list, period) 93 | |> MA.wma(period) 94 | end 95 | 96 | def get_avg(atr_list, new_tr, period, :ema) do 97 | if length(atr_list) == period - 1 do 98 | atr_list 99 | |> E.map(fn %{tr: tr} -> tr end) 100 | |> case do 101 | list -> [new_tr | list] 102 | end 103 | |> E.reduce(@zero, fn n, t -> D.add(t, U.dec(n)) end) 104 | |> D.div(period) 105 | else 106 | atr_list 107 | |> hd() 108 | |> M.get(:avg) 109 | |> case do 110 | last_tr -> MA.ema({last_tr, new_tr}, period) 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/ma.ex: -------------------------------------------------------------------------------- 1 | defmodule TradeIndicators.MA do 2 | alias TradeIndicators.Util, as: U 3 | alias Decimal, as: D 4 | alias Enum, as: E 5 | 6 | @zero D.new(0) 7 | 8 | def safe_at(list, n) do 9 | case E.at(list, n - 1) do 10 | nil -> @zero 11 | num when is_float(num) -> D.from_float(num) 12 | num when is_integer(num) -> D.new(num) 13 | num = %D{} -> num 14 | end 15 | end 16 | 17 | def rma(prev = %D{}, next = %D{}, num) when is_integer(num) and num > 1, 18 | do: D.add(next, D.mult(num - 1, prev)) |> D.div(num) 19 | 20 | def ema({prev, next}, num, factor \\ :"2/(N+1)") 21 | when is_integer(num) and next != nil and factor in [:"2/(N+1)", :"1/N"] do 22 | factor_a = 23 | case factor do 24 | :"2/(N+1)" -> D.div(2, D.add(num, 1)) 25 | :"1/N" -> D.div(1, num) 26 | end 27 | 28 | factor_b = D.sub(1, factor_a) 29 | D.add(D.mult(U.dec(next), factor_a), D.mult(U.dec(prev || 0), factor_b)) 30 | end 31 | 32 | def wma(series, period) 33 | when is_list(series) and length(series) > 0 and is_integer(period) and period > 1 do 34 | series = 35 | case period - length(series) do 36 | 0 -> series 37 | n when n < period -> series ++ for(_ <- 1..n, do: 0) 38 | _ -> E.take(series, period) 39 | end 40 | 41 | n_sum = 42 | for i <- 1..period, reduce: 0 do 43 | t -> D.mult(safe_at(series, i), period - (i - 1)) |> D.add(t) 44 | end 45 | 46 | D.div(n_sum, D.mult(period, D.div(D.add(period, 1), 2))) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/macd.ex: -------------------------------------------------------------------------------- 1 | defmodule TradeIndicators.MACD do 2 | use TypedStruct 3 | alias __MODULE__, as: MACD 4 | alias TradeIndicators.MA 5 | alias TradeIndicators.Util, as: U 6 | alias Decimal, as: D 7 | alias Enum, as: E 8 | alias List, as: L 9 | alias Map, as: M 10 | 11 | typedstruct do 12 | field :list, List.t(), default: [] 13 | field :chart_len, pos_integer(), default: 1_000 14 | field :ema1_len, pos_integer(), default: 12 15 | field :ema2_len, pos_integer(), default: 26 16 | field :ema3_len, pos_integer(), default: 9 17 | end 18 | 19 | typedstruct module: Item do 20 | field :ema1, D.t() | nil 21 | field :ema2, D.t() | nil 22 | field :macd, D.t() | nil 23 | field :sig, D.t() | nil 24 | field :his, D.t() | nil 25 | field :t, non_neg_integer() 26 | end 27 | 28 | defp get_prev_macd([]), do: {nil, nil, nil} 29 | 30 | defp get_prev_macd([last | _]) do 31 | case last do 32 | %Item{ema1: a, ema2: b, sig: c} -> {a, b, c} 33 | end 34 | end 35 | 36 | def sma({key, src_list}, len) when is_list(src_list) and is_integer(len) do 37 | case E.take(src_list, len) do 38 | subset -> 39 | case {length(subset), L.last(subset), subset} do 40 | {l, _, _} when l < len -> nil 41 | {_, %{^key => nil}, _} -> nil 42 | {_, _, subset} -> subset |> E.reduce(0, &D.add(M.get(&1, key), &2)) |> D.div(len) 43 | end 44 | end 45 | end 46 | 47 | def get_avg(tuple, avg_prev, len) do 48 | case {tuple, avg_prev, len} do 49 | {{_, [], _}, _, _} -> 50 | nil 51 | 52 | {{_, _, nil}, nil, _} -> 53 | nil 54 | 55 | {{key, src_list}, nil, len} 56 | when is_list(src_list) and is_integer(len) and is_atom(key) -> 57 | sma({key, src_list}, len) 58 | 59 | {{key, src_list, latest = %D{}}, nil, len} 60 | when is_list(src_list) and is_integer(len) and is_atom(key) -> 61 | sma({key, [%{key => latest} | src_list]}, len) 62 | 63 | {{_, _, latest = %D{}}, avg_prev = %D{}, len} 64 | when is_integer(len) -> 65 | MA.ema({avg_prev, latest}, len) 66 | 67 | {{key, src_list}, avg_prev = %D{}, len} 68 | when is_list(src_list) and is_integer(len) and is_atom(key) -> 69 | MA.ema({avg_prev, L.first(src_list)[key]}, len) 70 | end 71 | end 72 | 73 | defp dif(nil, _), do: nil 74 | defp dif(_, nil), do: nil 75 | defp dif(a, b), do: D.sub(U.dec(a), U.dec(b)) 76 | 77 | def step( 78 | macd_container = %MACD{ 79 | list: macd_list, 80 | chart_len: max_len, 81 | ema1_len: len1, 82 | ema2_len: len2, 83 | ema3_len: len3 84 | }, 85 | bars 86 | ) 87 | when is_list(bars) do 88 | {ema12_prev, ema26_prev, signal_prev} = get_prev_macd(macd_list) 89 | new_ema12_pt = get_avg({:c, bars}, ema12_prev, len1) 90 | new_ema26_pt = get_avg({:c, bars}, ema26_prev, len2) 91 | new_macd_line_pt = dif(new_ema12_pt, new_ema26_pt) 92 | new_signal_pt = get_avg({:macd, macd_list, new_macd_line_pt}, signal_prev, len3) 93 | new_histogram_pt = dif(new_macd_line_pt, new_signal_pt) 94 | 95 | new_macd_map = %Item{ 96 | ema1: new_ema12_pt, 97 | ema2: new_ema26_pt, 98 | macd: new_macd_line_pt, 99 | sig: new_signal_pt, 100 | his: new_histogram_pt, 101 | t: L.first(bars)[:t] 102 | } 103 | 104 | %{macd_container | list: E.take([new_macd_map | macd_list], max_len)} 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/rsi.ex: -------------------------------------------------------------------------------- 1 | defmodule TradeIndicators.RSI do 2 | use TypedStruct 3 | alias __MODULE__, as: RSI 4 | alias __MODULE__.Item 5 | alias TradeIndicators.MA 6 | alias TradeIndicators.Util, as: U 7 | alias Decimal, as: D 8 | alias Enum, as: E 9 | 10 | typedstruct do 11 | field :list, List.t(), default: [] 12 | field :period, pos_integer(), default: 14 13 | end 14 | 15 | typedstruct module: Item do 16 | field :value, D.t() | nil, default: nil 17 | field :avg_gain, D.t() | nil, default: nil 18 | field :avg_loss, D.t() | nil, default: nil 19 | field :gain, D.t() | nil, default: nil 20 | field :loss, D.t() | nil, default: nil 21 | field :t, non_neg_integer(), default: 0 22 | end 23 | 24 | @zero D.new(0) 25 | @one_hundred D.new(100) 26 | 27 | def step(chart = %RSI{}, bars) when is_list(bars) do 28 | case length(bars) do 29 | 0 -> chart 30 | 1 -> update_rsi_list(chart, bars) 31 | _ -> update_rsi_list(chart, E.take(bars, 2)) 32 | end 33 | end 34 | 35 | def update_rsi_list(rsi_chart = %RSI{list: []}, [%{t: ts}]), 36 | do: %{rsi_chart | list: [new_rsi_struct({nil, nil, nil}, @zero, @zero, ts)]} 37 | 38 | def update_rsi_list( 39 | rsi_chart = %RSI{list: rsi_list, period: len}, 40 | [%{c: close_new, t: ts}, %{c: close_old}] 41 | ) 42 | when is_list(rsi_list) do 43 | delta = D.sub(close_new, close_old) 44 | gain_now = delta |> D.max(0) 45 | loss_now = delta |> D.min(0) |> D.abs() 46 | 47 | case length(rsi_list) do 48 | l when l < len -> {nil, nil, nil} 49 | ^len -> get_initial_gain_loss(rsi_list, {gain_now, loss_now}, len) |> calc_rsi() 50 | _ -> calc_rs(rsi_list, gain_now, loss_now, len) |> calc_rsi() 51 | end 52 | |> new_rsi_struct(gain_now, loss_now, ts) 53 | |> case do 54 | new_rsi_item -> %{rsi_chart | list: [new_rsi_item | rsi_list]} 55 | end 56 | end 57 | 58 | def new_rsi_struct({rsi, avg_g, avg_l}, gain, loss, ts), 59 | do: %Item{value: rsi, avg_gain: avg_g, avg_loss: avg_l, gain: gain, loss: loss, t: ts} 60 | 61 | def calc_rs(rsi_list, gain_now, loss_now, len) do 62 | case hd(rsi_list) do 63 | %Item{avg_gain: gain_last, avg_loss: loss_last} -> 64 | {MA.rma(gain_last, gain_now, len), MA.rma(loss_last, loss_now, len)} 65 | end 66 | end 67 | 68 | def calc_rsi({avg_gain = %D{}, avg_loss = %D{}}) do 69 | cond do 70 | D.eq?(@zero, avg_loss) -> {@one_hundred, avg_gain, avg_loss} 71 | D.eq?(@zero, avg_gain) -> {@zero, avg_gain, avg_loss} 72 | true -> {D.sub(100, D.div(100, D.add(1, D.div(avg_gain, avg_loss)))), avg_gain, avg_loss} 73 | end 74 | end 75 | 76 | def get_initial_gain_loss(rsi_list, {gain_now, loss_now}, period) 77 | when is_list(rsi_list) and is_integer(period) and period > 1 do 78 | E.reduce(rsi_list, {0, 0}, fn %{gain: gain, loss: loss}, {total_gain, total_loss} -> 79 | {D.add(total_gain, U.dec(gain)), D.add(total_loss, U.dec(loss))} 80 | end) 81 | |> case do 82 | {g, l} -> {D.div(D.add(g, gain_now), period), D.div(D.add(l, loss_now), period)} 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/util.ex: -------------------------------------------------------------------------------- 1 | defmodule TradeIndicators.Util do 2 | alias Decimal, as: D 3 | alias Enum, as: E 4 | alias Map, as: M 5 | 6 | @zero D.new(0) 7 | 8 | def rnd(val), 9 | do: dec(val) |> D.round(2, :half_even) |> D.to_float() 10 | 11 | def dec(num) do 12 | case num do 13 | nil -> @zero 14 | num = %D{} -> num 15 | num when is_integer(num) -> D.new(num) 16 | num when is_float(num) -> D.from_float(num) 17 | end 18 | end 19 | 20 | def decimals(some_map) when is_map(some_map) do 21 | some_map 22 | |> E.map(fn {k, v} -> {k, dec(v)} end) 23 | |> M.new() 24 | end 25 | 26 | def context(func) when is_function(func), 27 | do: D.Context.with(%D.Context{precision: 8, rounding: :half_even}, func) 28 | end 29 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule TradeIndicators.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :trade_indicators, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | elixirc_paths: elixirc_paths(Mix.env()), 11 | deps: deps() 12 | ] 13 | end 14 | 15 | defp elixirc_paths(:test), do: ["lib", "test/support"] 16 | defp elixirc_paths(_), do: ["lib"] 17 | 18 | # Run "mix help compile.app" to learn about applications. 19 | def application do 20 | [ 21 | extra_applications: [:logger] 22 | ] 23 | end 24 | 25 | # Run "mix help deps" to learn about dependencies. 26 | defp deps do 27 | [ 28 | {:decimal, "~> 2.0"}, 29 | {:typed_struct, "~> 0.2.1"} 30 | ] 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, 3 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 4 | "typed_struct": {:hex, :typed_struct, "0.2.1", "e1993414c371f09ff25231393b6430bd89d780e2a499ae3b2d2b00852f593d97", [:mix], [], "hexpm", "8f5218c35ec38262f627b2c522542f1eae41f625f92649c0af701a6fab2e11b3"}, 5 | } 6 | -------------------------------------------------------------------------------- /test/atr_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TradeIndicators.Tests.ATR do 2 | use ExUnit.Case 3 | alias TradeIndicators.Util, as: U 4 | alias TradeIndicators.ATR 5 | alias Enum, as: E 6 | 7 | @msft_data TradeIndicators.Tests.Fixtures.get(:msft_m1_2020_07_27) 8 | @tr [0.21, 0.18, 0.31, 0.15, 0.20, 0.15, 0.28, 0.20, 0.19, 0.17] ++ 9 | [0.23, 0.37, 0.21, 0.16, 0.29, 0.15, 0.42, 0.27, 0.11, 0.23] ++ 10 | [0.41, 0.63, 0.25, 0.43, 0.68, 0.48, 0.56, 0.56, 0.42, 0.68] ++ 11 | [1.25, 0.40, 0.69, 0.09, 0.15, 0.21, 0.14, 0.08, 1.35, 0.00] 12 | @wma_atr [0.21, 0.21, 0.22, 0.21, 0.22, 0.22, 0.23, 0.23, 0.24, 0.25] ++ 13 | [0.27, 0.28, 0.28, 0.30, 0.33, 0.34, 0.38, 0.39, 0.42, 0.48] ++ 14 | [0.52, 0.54, 0.52, 0.55, 0.55, 0.53, 0.53, 0.00, 0.00, 0.00] ++ 15 | [0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00] 16 | @ema_atr [0.23, 0.23, 0.24, 0.23, 0.24, 0.25, 0.27, 0.26, 0.27, 0.29] ++ 17 | [0.30, 0.31, 0.31, 0.32, 0.34, 0.35, 0.38, 0.38, 0.40, 0.44] ++ 18 | [0.47, 0.48, 0.46, 0.49, 0.50, 0.47, 0.47, 0.00, 0.00, 0.00] ++ 19 | [0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00] 20 | 21 | describe "ATR" do 22 | test "true range calculations and weighted moving average" do 23 | U.context(fn -> 24 | atr_list = 25 | @msft_data 26 | |> Enum.reduce({%ATR{method: :wma}, []}, fn bar, {state, bars} -> 27 | bars = [bar | bars] 28 | {ATR.step(state, bars), bars} 29 | end) 30 | |> case do 31 | {%{list: atr_list}, _} -> atr_list 32 | end 33 | 34 | tr_results = E.map(atr_list, fn %{tr: v} -> U.rnd(v) end) 35 | assert tr_results == @tr 36 | 37 | atr_results = E.map(atr_list, fn %{avg: v} -> U.rnd(v) end) 38 | assert atr_results == @wma_atr 39 | end) 40 | end 41 | 42 | test "exponential moving average" do 43 | U.context(fn -> 44 | atr_list = 45 | @msft_data 46 | |> Enum.reduce({%ATR{method: :ema}, []}, fn bar, {state, bars} -> 47 | bars = [bar | bars] 48 | {ATR.step(state, bars), bars} 49 | end) 50 | |> case do 51 | {%{list: atr_list}, _} -> atr_list 52 | end 53 | 54 | atr_results = 55 | E.map(atr_list, fn 56 | %{avg: nil} -> 0.0 57 | %{avg: v} -> U.rnd(v) 58 | end) 59 | 60 | assert @ema_atr == atr_results 61 | end) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/ma_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TradeIndicators.Tests.MovingAverage do 2 | use ExUnit.Case 3 | alias TradeIndicators.Util, as: U 4 | alias TradeIndicators.MA 5 | alias Decimal, as: D 6 | 7 | @fixture [0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 8 | 9 | test "linear weighted average" do 10 | U.context(fn -> 11 | assert @fixture |> MA.wma(14) |> U.rnd() == 0.07 12 | 13 | assert MA.wma([3], 14) |> D.to_float() == 0.4 14 | assert MA.wma([2, 2, 1], 5) |> D.to_float() == 1.4 15 | assert MA.wma([0, 4, 3, 2, 1], 5) |> D.to_float() == 2.0 16 | end) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/macd_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TradeIndicators.Tests.MACD do 2 | use ExUnit.Case 3 | alias TradeIndicators.MACD 4 | alias TradeIndicators.Util, as: U 5 | alias Decimal, as: D 6 | alias Enum, as: E 7 | 8 | @msft_data TradeIndicators.Tests.Fixtures.get(:msft_m1_2020_07_27) 9 | @histogram [0.06, 0.06, 0.06, 0.06, 0.03, 0.01, -0.01] 10 | @signal [0.13, 0.14, 0.16, 0.17, 0.18, 0.18, 0.18] 11 | @macd [0.12, 0.10, 0.10, 0.11, 0.12, 0.12, 0.14, 0.16, 0.19, 0.20, 0.22, 0.23, 0.22, 0.19, 0.17] 12 | 13 | describe "MACD" do 14 | test "sma/3" do 15 | list = for(i <- 1..3, do: %{a: i}) 16 | assert nil == MACD.sma({:a, list}, 4) 17 | assert MACD.sma({:a, list}, 3) |> D.eq?(2) 18 | end 19 | 20 | test "get_avg/3" do 21 | nil_list = for(_ <- 1..5, do: %{a: nil}) 22 | num_list = for(i <- 1..5, do: %{a: i}) 23 | 24 | assert MACD.get_avg({:a, nil_list}, nil, 5) == nil 25 | assert MACD.get_avg({:a, nil_list}, nil, 5) == nil 26 | assert MACD.get_avg({:a, num_list}, nil, 5) == D.new(3) 27 | 28 | list = for(i <- 1..11, do: %{a: i}) 29 | assert MACD.get_avg({:a, list}, nil, 12) == nil 30 | 31 | list = for(i <- 1..12, do: %{a: i}) 32 | assert MACD.get_avg({:a, list}, nil, 12) == D.from_float(6.5) 33 | end 34 | 35 | test "values on MSFT 2020/07/31" do 36 | U.context(fn -> 37 | macd_list = 38 | @msft_data 39 | |> E.reduce({%MACD{}, []}, fn bar, {state, bars} -> 40 | bars = [bar | bars] 41 | state = MACD.step(state, bars) 42 | {state, bars} 43 | end) 44 | |> case do 45 | {%{list: macd_list}, _} -> macd_list |> E.reverse() 46 | end 47 | 48 | n1 = E.at(macd_list, 0) 49 | n11 = E.at(macd_list, 10) 50 | n12 = E.at(macd_list, 11) 51 | n25 = E.at(macd_list, 24) 52 | n26 = E.at(macd_list, 25) 53 | n33 = E.at(macd_list, 32) 54 | n34 = E.at(macd_list, 33) 55 | n40 = E.at(macd_list, 39) 56 | assert match?(%{ema1: nil, ema2: nil, macd: nil, his: nil, sig: nil}, n1) 57 | assert match?(%{ema1: nil, ema2: nil, macd: nil, his: nil, sig: nil}, n11) 58 | assert match?(%{ema1: %D{}, ema2: nil, macd: nil, his: nil, sig: nil}, n12) 59 | assert match?(%{ema1: %D{}, ema2: nil, macd: nil, his: nil, sig: nil}, n25) 60 | assert match?(%{ema1: %D{}, ema2: %D{}, macd: %D{}, his: nil, sig: nil}, n26) 61 | assert match?(%{ema1: %D{}, ema2: %D{}, macd: %D{}, his: nil, sig: nil}, n33) 62 | assert match?(%{ema1: %D{}, ema2: %D{}, macd: %D{}, his: %D{}, sig: %D{}}, n34) 63 | assert match?(%{ema1: %D{}, ema2: %D{}, macd: %D{}, his: %D{}, sig: %D{}}, n40) 64 | 65 | {macd_result, histogram_result, signal_result} = 66 | macd_list 67 | |> E.reduce({[], [], []}, fn %{macd: m, his: h, sig: s}, {a, b, c} -> 68 | a = if(is_nil(m), do: a, else: a ++ [U.rnd(m)]) 69 | b = if(is_nil(h), do: b, else: b ++ [U.rnd(h)]) 70 | c = if(is_nil(s), do: c, else: c ++ [U.rnd(s)]) 71 | {a, b, c} 72 | end) 73 | 74 | assert @macd == macd_result 75 | assert @histogram == histogram_result 76 | assert @signal == signal_result 77 | end) 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/rsi_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TradeIndicators.Tests.RSI do 2 | use ExUnit.Case 3 | alias TradeIndicators.Util, as: U 4 | alias TradeIndicators.RSI 5 | alias Enum, as: E 6 | 7 | @rsi_expected [72.06, 69.65, 71.07, 71.07, 66.50, 62.21, 62.52, 62.81, 63.08, 60.02] ++ 8 | [56.50, 53.65, 52.50, 55.95, 54.02, 57.19, 56.71, 61.77, 60.82, 57.78] ++ 9 | [52.29, 52.65, 65.01, 62.22, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] ++ 10 | [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 11 | @msft_data TradeIndicators.Tests.Fixtures.get(:msft_m1_2020_08_17) 12 | 13 | describe "RSI" do 14 | test "step/2" do 15 | U.context(fn -> 16 | result_list = 17 | E.reduce(@msft_data, {%RSI{}, []}, fn bar, {state, bars} -> 18 | bars = [bar | bars] 19 | {RSI.step(state, bars), bars} 20 | end) 21 | |> case do 22 | {%{list: list}, _} -> E.map(list, fn %{value: v} -> U.rnd(v) end) 23 | end 24 | 25 | assert @rsi_expected == result_list 26 | end) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/support/fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule TradeIndicators.Tests.Fixtures do 2 | alias TradeIndicators.Util, as: U 3 | alias Enum, as: E 4 | alias Map, as: M 5 | 6 | @msft_m1_2020_07_27 [ 7 | %{t: 1_595_620_860, o: 201.63, c: 201.63, h: 201.63, l: 201.63}, 8 | %{t: 1_595_852_820, o: 202.98, c: 202.98, h: 202.98, l: 202.98}, 9 | %{t: 1_595_853_360, o: 202.90, c: 202.90, h: 202.90, l: 202.90}, 10 | %{t: 1_595_854_020, o: 202.76, c: 202.76, h: 202.76, l: 202.76}, 11 | %{t: 1_595_854_200, o: 202.55, c: 202.55, h: 202.55, l: 202.55}, 12 | %{t: 1_595_855_580, o: 202.40, c: 202.40, h: 202.40, l: 202.40}, 13 | %{t: 1_595_856_300, o: 202.31, c: 202.31, h: 202.31, l: 202.31}, 14 | %{t: 1_595_856_480, o: 202.01, c: 201.93, h: 202.01, l: 201.62}, 15 | %{t: 1_595_856_540, o: 201.53, c: 201.59, h: 201.60, l: 201.53}, 16 | %{t: 1_595_856_600, o: 201.41, c: 202.46, h: 202.66, l: 201.41}, 17 | %{t: 1_595_856_660, o: 202.52, c: 201.94, h: 202.56, l: 201.88}, 18 | %{t: 1_595_856_720, o: 201.93, c: 201.54, h: 201.96, l: 201.54}, 19 | %{t: 1_595_856_780, o: 201.55, c: 201.90, h: 201.98, l: 201.42}, 20 | %{t: 1_595_856_840, o: 201.96, c: 202.22, h: 202.46, l: 201.95}, 21 | %{t: 1_595_856_900, o: 202.27, c: 202.62, h: 202.70, l: 202.27}, 22 | %{t: 1_595_856_960, o: 202.55, c: 202.22, h: 202.55, l: 201.94}, 23 | %{t: 1_595_857_020, o: 202.30, c: 202.55, h: 202.65, l: 202.29}, 24 | %{t: 1_595_857_080, o: 202.59, c: 202.68, h: 202.75, l: 202.50}, 25 | %{t: 1_595_857_140, o: 202.66, c: 202.44, h: 202.74, l: 202.11}, 26 | %{t: 1_595_857_200, o: 202.42, c: 202.56, h: 202.61, l: 202.20}, 27 | %{t: 1_595_857_260, o: 202.60, c: 202.56, h: 202.65, l: 202.42}, 28 | %{t: 1_595_857_320, o: 202.59, c: 202.60, h: 202.65, l: 202.54}, 29 | %{t: 1_595_857_380, o: 202.61, c: 202.47, h: 202.71, l: 202.44}, 30 | %{t: 1_595_857_440, o: 202.42, c: 202.55, h: 202.55, l: 202.13}, 31 | %{t: 1_595_857_500, o: 202.56, c: 202.54, h: 202.63, l: 202.48}, 32 | %{t: 1_595_857_560, o: 202.55, c: 202.44, h: 202.60, l: 202.31}, 33 | %{t: 1_595_857_620, o: 202.48, c: 202.43, h: 202.58, l: 202.42}, 34 | %{t: 1_595_857_680, o: 202.48, c: 202.47, h: 202.58, l: 202.37}, 35 | %{t: 1_595_857_740, o: 202.41, c: 202.76, h: 202.76, l: 202.39}, 36 | %{t: 1_595_857_800, o: 202.74, c: 202.69, h: 202.92, l: 202.69}, 37 | %{t: 1_595_857_860, o: 202.62, c: 202.74, h: 202.79, l: 202.62}, 38 | %{t: 1_595_857_920, o: 202.76, c: 202.88, h: 202.89, l: 202.70}, 39 | %{t: 1_595_857_980, o: 202.90, c: 203.02, h: 203.04, l: 202.84}, 40 | %{t: 1_595_858_040, o: 203.05, c: 203.18, h: 203.23, l: 202.95}, 41 | %{t: 1_595_858_100, o: 203.13, c: 203.10, h: 203.16, l: 203.03}, 42 | %{t: 1_595_858_160, o: 203.17, c: 203.22, h: 203.30, l: 203.16}, 43 | %{t: 1_595_858_220, o: 203.19, c: 203.21, h: 203.24, l: 203.09}, 44 | %{t: 1_595_858_280, o: 203.21, c: 202.96, h: 203.21, l: 202.90}, 45 | %{t: 1_595_858_340, o: 202.94, c: 202.82, h: 203.00, l: 202.82}, 46 | %{t: 1_595_858_400, o: 202.81, c: 202.80, h: 203.01, l: 202.80} 47 | ] 48 | |> E.map(&(&1 |> M.drop([:t]) |> U.decimals() |> M.put(:t, &1[:t]))) 49 | 50 | @msft_m1_2020_08_17 [ 51 | %{t: 1_597_426_564, o: 208.69, c: 208.69, h: 208.69, l: 208.69}, 52 | %{t: 1_597_653_724, o: 209.36, c: 209.36, h: 209.36, l: 209.36}, 53 | %{t: 1_597_654_924, o: 209.55, c: 209.55, h: 209.55, l: 209.55}, 54 | %{t: 1_597_655_044, o: 209.55, c: 209.50, h: 209.55, l: 209.50}, 55 | %{t: 1_597_655_224, o: 209.50, c: 209.50, h: 209.50, l: 209.50}, 56 | %{t: 1_597_656_184, o: 209.50, c: 209.50, h: 209.50, l: 209.50}, 57 | %{t: 1_597_656_364, o: 209.50, c: 209.50, h: 209.50, l: 209.50}, 58 | %{t: 1_597_656_604, o: 209.60, c: 209.52, h: 209.68, l: 209.50}, 59 | %{t: 1_597_656_664, o: 209.60, c: 209.84, h: 210.16, l: 209.60}, 60 | %{t: 1_597_656_724, o: 209.80, c: 210.09, h: 210.12, l: 209.79}, 61 | %{t: 1_597_656_784, o: 210.09, c: 209.69, h: 210.11, l: 209.62}, 62 | %{t: 1_597_656_844, o: 209.78, c: 209.62, h: 210.00, l: 209.62}, 63 | %{t: 1_597_656_904, o: 209.75, c: 209.85, h: 209.89, l: 209.69}, 64 | %{t: 1_597_656_964, o: 209.86, c: 209.71, h: 209.91, l: 209.48}, 65 | %{t: 1_597_657_024, o: 209.72, c: 209.35, h: 209.72, l: 209.25}, 66 | %{t: 1_597_657_084, o: 209.51, c: 209.55, h: 209.72, l: 209.47}, 67 | %{t: 1_597_657_144, o: 209.56, c: 208.96, h: 209.70, l: 208.96}, 68 | %{t: 1_597_657_204, o: 209.03, c: 208.94, h: 209.25, l: 208.94}, 69 | %{t: 1_597_657_264, o: 208.97, c: 209.29, h: 209.32, l: 208.97}, 70 | %{t: 1_597_657_324, o: 209.31, c: 209.51, h: 209.56, l: 209.31}, 71 | %{t: 1_597_657_384, o: 209.49, c: 209.58, h: 209.71, l: 209.44}, 72 | %{t: 1_597_657_444, o: 209.59, c: 209.34, h: 209.63, l: 209.34}, 73 | %{t: 1_597_657_504, o: 209.30, c: 209.37, h: 209.49, l: 209.30}, 74 | %{t: 1_597_657_564, o: 209.54, c: 209.22, h: 209.54, l: 209.19}, 75 | %{t: 1_597_657_624, o: 209.35, c: 209.33, h: 209.40, l: 209.26}, 76 | %{t: 1_597_657_684, o: 209.33, c: 209.17, h: 209.34, l: 209.15}, 77 | %{t: 1_597_657_744, o: 209.17, c: 209.23, h: 209.37, l: 209.16}, 78 | %{t: 1_597_657_804, o: 209.34, c: 209.38, h: 209.50, l: 209.34}, 79 | %{t: 1_597_657_864, o: 209.44, c: 209.58, h: 209.63, l: 209.44}, 80 | %{t: 1_597_657_924, o: 209.64, c: 209.77, h: 209.82, l: 209.64}, 81 | %{t: 1_597_657_984, o: 209.78, c: 209.76, h: 209.79, l: 209.73}, 82 | %{t: 1_597_658_044, o: 209.77, c: 209.75, h: 209.80, l: 209.70}, 83 | %{t: 1_597_658_104, o: 209.76, c: 209.74, h: 209.78, l: 209.73}, 84 | %{t: 1_597_658_164, o: 209.72, c: 209.98, h: 209.98, l: 209.72}, 85 | %{t: 1_597_658_224, o: 209.98, c: 210.29, h: 210.29, l: 209.98}, 86 | %{t: 1_597_658_284, o: 210.33, c: 210.29, h: 210.33, l: 210.16}, 87 | %{t: 1_597_658_344, o: 210.28, c: 210.25, h: 210.37, l: 210.20}, 88 | %{t: 1_597_658_404, o: 210.36, c: 210.41, h: 210.50, l: 210.34} 89 | ] 90 | |> E.map(&(&1 |> M.drop([:t]) |> U.decimals() |> M.put(:t, &1[:t]))) 91 | 92 | def get(:msft_m1_2020_07_27), 93 | do: @msft_m1_2020_07_27 94 | 95 | def get(:msft_m1_2020_08_17), 96 | do: @msft_m1_2020_08_17 97 | end 98 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------