Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added bollinger bands as an additional indicator #8

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ It's currently a work in progress; updates will be pushed on a regular basis.
- Indicator
- MACD (Moving Average Convergence Divergence)
- RSI (Relative Strength Index)
- Bollinger Bands

## Why?

Expand Down
41 changes: 39 additions & 2 deletions lib/talib/average.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ defmodule Talib.Average do
"""
@spec mean([number]) :: {:ok, number} | {:error, atom}
def mean([]), do: {:error, :no_data}
def mean(data), do: {:ok, Enum.sum(data) / length(data)}
def mean(data) do
{:ok, Enum.filter(data, &(Kernel.!=(&1, nil)))
|> (fn(x) -> Enum.sum(x)/Enum.count(x) end).()
}
end

@doc """
Gets the median of a list.
Expand Down Expand Up @@ -192,7 +196,10 @@ defmodule Talib.Average do
"""
@spec mean!([number]) :: number | no_return
def mean!([]), do: raise NoDataError
def mean!(data), do: Enum.sum(data) / length(data)
def mean!(data) do
Enum.filter(data, &(Kernel.!=(&1, nil)))
|> (fn(x) -> Enum.sum(x)/Enum.count(x) end).()
end

@doc """
Gets the median of a list.
Expand Down Expand Up @@ -319,6 +326,36 @@ defmodule Talib.Average do
|> map_max
end

@doc false
@spec deviation!([number]) :: [number, ...] | number | no_return
def deviation!([]), do: raise NoDataError
def deviation!(data) do
m = data
|> mean!

deviation_reduce(data, m)
end

defp deviation_reduce(data, m) do
data
|> Enum.filter(&Kernel.!=(&1, nil))
|> Enum.map(&:math.pow(&1 - m, 2))
|> (fn (x) -> Enum.sum(x)/Enum.count(x) end).()
|> :math.sqrt()
end

@doc false
@spec deviation([number]) :: [number, ...] | number | {:error, atom}
def deviation([]), do: {:error, :no_data}
def deviation(data) do
case data |> mean do
{:ok, m} -> deviation_reduce(data, m)
{:error, reason} -> {:error, reason}
end
end



@doc false
@spec map_max(map()) :: number | [number, ...]
defp map_max(map) do
Expand Down
159 changes: 159 additions & 0 deletions lib/talib/bollinger_band.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
defmodule Talib.BollingerBand do
alias Talib.SMA
alias Talib.Average
require OK
require Logger

@moduledoc ~S"""
Defines a Bollinger bands.

## History

Version: 1.0
https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:bollinger_bands
Audited by:

| Name | Title |
| :----------- | :---------------- |
| | |

"""

@typedoc """
Defines a Bollinger Band price volatility.
* :period - Period used to calculate SMA, typically 20
* :deviation - Multiplier to standard deviation from SMA typically 2
* :values - List of values resulting from the calculation {upper, middle, lower}
"""
@type t :: %Talib.BollingerBand{
period: integer,
deviation: integer,
values: [number]
}

defstruct [
period: 0,
deviation: 0,
values: [],
]

@doc """
Gets the BBand of a list.

The return tuple looks like the following: {MACD, MACD Signal}.
Raises `NoDataError` if the given list is an empty list.

## Examples

iex>Talib.BollingerBand.from_list([1, 2, 3, 4, 5, 6], 3, 2)
{:ok, %Talib.BollingerBand{
period: 3,
deviation: 2,
values: [
{nil, nil, nil},
{nil, nil, nil},
{3.0, 2.0, 1.0},
{4.6329931618554525, 3.0, 1.367006838144548},
{5.6329931618554525, 4.0, 2.367006838144548},
{6.6329931618554525, 5.0, 3.367006838144548}
]
}}

iex>Talib.BollingerBand.from_list([], 3, 2)
{:error, :no_data}
"""
@spec from_list([number], integer, integer) :: {:ok, Talib.BollingerBand.t}
| {:error, atom}
def from_list(data, period \\ 20, deviation \\ 2),
do: calculate(data, period, deviation)

@doc """
Gets the BBand of a list.

The return tuple looks like the following: {Upper Band, Middle, Lower Band}.
Raises `NoDataError` if the given list is an empty list.

## Examples

iex>Talib.BollingerBand.from_list!([1, 2, 3], 3, 2)
%Talib.BollingerBand{
deviation: 2,
period: 3,
values: [
{nil, nil, nil},
{nil, nil, nil},
{3.0, 2.0, 1.0}
]
}

iex>Talib.BollingerBand.from_list!([], 20, 2)
** (NoDataError) no data error
"""
@spec from_list!([number], integer, integer) :: Talib.BBand.t
| no_return
def from_list!(data, period \\ 20, deviation \\ 2) do
case calculate(data, period, deviation) do
{:ok, result} -> result
{:error, :no_data} -> raise NoDataError
end
end

defp calculate_bband_point(mid, stddev, deviation) when is_nil(mid) do
{nil, nil, nil}
end

defp calculate_bband_point(mid, stddev, deviation) when is_nil(stddev) do
{nil, nil, nil}
end

defp calculate_bband_point(mid, stddev, deviation) when is_float(stddev) and is_float(mid) do
band = (stddev * deviation)
{mid + band, mid, mid - band}
end

defp calculate_bband_point(mid, stddev_series, deviation) when is_list(stddev_series) do
stddev = Average.deviation!(stddev_series)
calculate_bband_point(mid, stddev, deviation)
end

@doc false
@spec calculate([number], integer, integer) :: {:ok, Talib.BollingerBand.t}
| {:error, atom}
defp calculate(data, period, deviation) do
OK.with do
%SMA{values: middle_band} <- SMA.from_list(data, period)



bband_ = data
|> Enum.reverse
|> Enum.chunk_every(period, 1, [nil])
|> Enum.reverse
|> Enum.map(&Enum.reverse(&1))

deficit = length(data) - length(bband_)
empty = Stream.cycle([nil])
|> Enum.take(period)

bband = Stream.cycle([empty])
|> Enum.take(deficit)
|> Kernel.++(bband_)
|> Enum.zip(middle_band)
|> Enum.map(fn({series, m}) -> calculate_bband_point(m, series, deviation) end)

#bband = Enum.chunk_every(shaped_data, period, 1, [7])
#IO.inspect bband, limit: :infinity
#IO.inspect length(bband)
#|> Enum.zip(middle_band)
#|> Enum.map(fn({series, m}) -> calculate_bband_point(m, series, deviation) end)

{:ok, %Talib.BollingerBand{
period: period,
deviation: deviation,
values: bband,
}}
else
:no_data -> {:error, :no_data}
end
end
end
2 changes: 2 additions & 0 deletions lib/talib/indicator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
defmodule Talib.Indicator do
alias Talib.MovingAverage
alias Talib.Utility
alias Talib.SMA

@moduledoc ~S"""
Module containing indicator functions, such as the RSI.
Expand Down Expand Up @@ -88,4 +89,5 @@ defmodule Talib.Indicator do
100 - 100 / (relative_strength + 1)
end
end

end
4 changes: 2 additions & 2 deletions lib/talib/sma.ex
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ defmodule Talib.SMA do
length(data) >= period ->
result = data
|> Enum.take(period)
|> Enum.sum
|> Kernel./(period)
|> Enum.filter(&(Kernel.!=(&1, nil)))
|> (fn(x) -> Enum.sum(x)/Enum.count(x) end).()

calculate(tl, period, results ++ [result])
end
Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ defmodule Talib.Mixfile do
#
# Type "mix help deps" for more examples and options
defp deps do
[{:ok, "~> 1.6"}]
[{:ok, "~> 1.6"},
]
end
end
1 change: 0 additions & 1 deletion mix.lock

This file was deleted.

8 changes: 8 additions & 0 deletions test/average_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defmodule Talib.AverageTest do
def numbers_median, do: (53 + 46) / 2
def numbers_mode, do: [30, 53, 89]
def numbers_midrange, do: (6 + 100) / 2
def numbers_deviation, do: 23.902458032595728
end

test "mean/1" do
Expand Down Expand Up @@ -67,4 +68,11 @@ defmodule Talib.AverageTest do
assert Average.mode!([3]) == 3
assert_raise NoDataError, fn -> Average.mode!([]) end
end

test "deviation!/1" do
assert Average.deviation!(Fixtures.numbers) == Fixtures.numbers_deviation
assert Average.deviation!([3]) == 0.0
assert_raise NoDataError, fn -> Average.deviation!([]) end
end

end
Loading