Skip to content
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# technical-analysis

A Ruby library for performing technical analysis on stock prices and other data sets.

## Indicators

The following technical indicators are supported:

- Accumulation/Distribution Index (ADI)
- Average Daily Trading Volume (ADTV)
- Average Directional Index (ADX)
Expand Down Expand Up @@ -57,6 +60,7 @@ gem install technical-analysis
```

## Usage

First, for the sake of these code samples, we'll load some test data from `spec/ta_test_data.csv`. This is the same data used for the unit tests. The data will be an `Array` of `Hashes`.

```ruby
Expand All @@ -70,6 +74,7 @@ input_data = SpecHelper.get_test_data(:close)
```

Each technical indicator has the following methods:

- `calculate` - Each technical indicator returns an Array of values. These values are instances of a class specific to each indicator. It's typically in the format of SymbolValue. For example, Simple Moving Average (SMA) returns an Array of `SmaValue` instances. These classes contain the appropriate data fields for each technical indicator.
- `indicator_symbol` returns the symbol of the technical indicator as a String.
- `indicator_name` returns the name of the technical indicator as a String.
Expand All @@ -78,6 +83,7 @@ Each technical indicator has the following methods:
- `min_data_size` returns the minimum number of observations needed (as an Integer) to calculate the technical indicator based on the options provided.

### Class-Based Usage

You can call methods on the class of the specific technical indicator that you want to calculate. To calculate a Simple Moving Average, for example, you would just call `calculate` on the Simple Moving Average class like so:

```ruby
Expand Down Expand Up @@ -108,9 +114,11 @@ TechnicalAnalysis::Sma.min_data_size(options)
```

### Generic Usage

You can also use the generic indicator class. The purpose of this class is to be a sort of master class that will find and call the correct indicator based on the params provided to it.

The `calculate` method on the `Indicator` class accepts:

- The indicator symbol as a String - `"sma"`
- The data to be used for calculations as an Array of Hashes - `input_data`
- The calculation to be performed as a Symbol - `:technicals`
Expand Down Expand Up @@ -174,7 +182,9 @@ simple_moving_average.min_data_size(options)
```

## Further documentation

This gem is also documented using [Yard](https://yardoc.org/). You can view the [guides](https://yardoc.org/guides/index.html) to help get you started.

## Run Tests

`rspec spec`
4 changes: 2 additions & 2 deletions lib/technical_analysis/indicators/bb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def self.calculate(data, period: 20, standard_deviations: 2, price_key: :value)
Validation.validate_length(data, min_data_size(period: period))
Validation.validate_date_time_key(data)

data = data.sort_by { |row| row[:date_time] }
data.sort_by! { |row| row[:date_time] } # Sort in place to save memory

output = []
period_values = []
Expand All @@ -85,7 +85,7 @@ def self.calculate(data, period: 20, standard_deviations: 2, price_key: :value)
end
end

output.sort_by(&:date_time).reverse
output.reverse!
end

end
Expand Down
172 changes: 89 additions & 83 deletions spec/technical_analysis/indicators/bb_spec.rb
Original file line number Diff line number Diff line change
@@ -1,100 +1,106 @@
# frozen_string_literal: true
require 'technical-analysis'
require 'spec_helper'

describe 'Indicators' do
describe "BB" do
input_data = SpecHelper.get_test_data(:close)
indicator = TechnicalAnalysis::Bb
RSpec.describe 'Indicators::Bollinger Bands (BB)' do
let(:input_data) { SpecHelper.get_test_data(:close) }
let(:indicator) { TechnicalAnalysis::Bb }

describe 'Bollinger Bands' do
it 'Calculates BB (20, 2)' do
output = indicator.calculate(input_data, period: 20, standard_deviations: 2, price_key: :close)
normalized_output = output.map(&:to_hash)
describe 'Bollinger Bands' do
it 'calculates BB (20, 2)' do
output = indicator.calculate(input_data, period: 20, standard_deviations: 2, price_key: :close)
normalized_output = output.map(&:to_hash)

expected_output = [
{:date_time=>"2019-01-09T00:00:00.000Z", :lower_band=>141.02036711220762, :middle_band=>157.35499999999996, :upper_band=>173.6896328877923},
{:date_time=>"2019-01-08T00:00:00.000Z", :lower_band=>141.07714470666247, :middle_band=>158.1695, :upper_band=>175.26185529333753},
{:date_time=>"2019-01-07T00:00:00.000Z", :lower_band=>141.74551015326722, :middle_band=>159.05649999999997, :upper_band=>176.36748984673272},
{:date_time=>"2019-01-04T00:00:00.000Z", :lower_band=>142.5717393007821, :middle_band=>160.39600000000002, :upper_band=>178.22026069921793},
{:date_time=>"2019-01-03T00:00:00.000Z", :lower_band=>143.53956406332316, :middle_band=>161.8175, :upper_band=>180.09543593667684},
{:date_time=>"2019-01-02T00:00:00.000Z", :lower_band=>145.3682834538487, :middle_band=>163.949, :upper_band=>182.52971654615132},
{:date_time=>"2018-12-31T00:00:00.000Z", :lower_band=>145.53555575730587, :middle_band=>164.98199999999997, :upper_band=>184.42844424269407},
{:date_time=>"2018-12-28T00:00:00.000Z", :lower_band=>145.90334076589886, :middle_band=>166.0725, :upper_band=>186.24165923410112},
{:date_time=>"2018-12-27T00:00:00.000Z", :lower_band=>146.65592111904317, :middle_band=>167.308, :upper_band=>187.96007888095681},
{:date_time=>"2018-12-26T00:00:00.000Z", :lower_band=>148.0390209273478, :middle_band=>168.2125, :upper_band=>188.38597907265222},
{:date_time=>"2018-12-24T00:00:00.000Z", :lower_band=>149.41938426834125, :middle_band=>169.08499999999998, :upper_band=>188.7506157316587},
{:date_time=>"2018-12-21T00:00:00.000Z", :lower_band=>153.6905118237551, :middle_band=>170.35799999999998, :upper_band=>187.02548817624486},
{:date_time=>"2018-12-20T00:00:00.000Z", :lower_band=>157.58081627897096, :middle_band=>171.6605, :upper_band=>185.74018372102907},
{:date_time=>"2018-12-19T00:00:00.000Z", :lower_band=>160.2737711222648, :middle_band=>172.66799999999998, :upper_band=>185.06222887773515},
{:date_time=>"2018-12-18T00:00:00.000Z", :lower_band=>161.48722339827833, :middle_band=>173.91649999999998, :upper_band=>186.34577660172164},
{:date_time=>"2018-12-17T00:00:00.000Z", :lower_band=>160.6411151779543, :middle_band=>175.28949999999995, :upper_band=>189.9378848220456},
{:date_time=>"2018-12-14T00:00:00.000Z", :lower_band=>161.3586392867227, :middle_band=>176.66299999999998, :upper_band=>191.96736071327726},
{:date_time=>"2018-12-13T00:00:00.000Z", :lower_band=>162.73753871102127, :middle_band=>177.72899999999998, :upper_band=>192.7204612889787},
{:date_time=>"2018-12-12T00:00:00.000Z", :lower_band=>162.83769519003326, :middle_band=>178.79299999999998, :upper_band=>194.7483048099667},
{:date_time=>"2018-12-11T00:00:00.000Z", :lower_band=>163.37450359253498, :middle_band=>180.04649999999998, :upper_band=>196.71849640746498},
{:date_time=>"2018-12-10T00:00:00.000Z", :lower_band=>162.797082234342, :middle_band=>181.8385, :upper_band=>200.879917765658},
{:date_time=>"2018-12-07T00:00:00.000Z", :lower_band=>162.2270311355715, :middle_band=>183.783, :upper_band=>205.33896886442847},
{:date_time=>"2018-12-06T00:00:00.000Z", :lower_band=>162.58630667652835, :middle_band=>185.856, :upper_band=>209.12569332347164},
{:date_time=>"2018-12-04T00:00:00.000Z", :lower_band=>163.34919566513827, :middle_band=>187.30850000000004, :upper_band=>211.2678043348618},
{:date_time=>"2018-12-03T00:00:00.000Z", :lower_band=>164.3311203741903, :middle_band=>188.55350000000004, :upper_band=>212.77587962580978},
{:date_time=>"2018-11-30T00:00:00.000Z", :lower_band=>164.11704019466114, :middle_band=>189.68650000000005, :upper_band=>215.25595980533896},
{:date_time=>"2018-11-29T00:00:00.000Z", :lower_band=>163.04822623308377, :middle_band=>191.8685, :upper_band=>220.68877376691626},
{:date_time=>"2018-11-28T00:00:00.000Z", :lower_band=>163.2435966888823, :middle_band=>193.83400000000003, :upper_band=>224.42440331111777},
{:date_time=>"2018-11-27T00:00:00.000Z", :lower_band=>164.31484291109825, :middle_band=>195.45200000000006, :upper_band=>226.58915708890186},
{:date_time=>"2018-11-26T00:00:00.000Z", :lower_band=>167.03813268520582, :middle_band=>197.35200000000003, :upper_band=>227.66586731479424},
{:date_time=>"2018-11-23T00:00:00.000Z", :lower_band=>169.9836589081704, :middle_band=>199.436, :upper_band=>228.8883410918296},
{:date_time=>"2018-11-21T00:00:00.000Z", :lower_band=>173.9574856242928, :middle_band=>201.81150000000002, :upper_band=>229.66551437570726},
{:date_time=>"2018-11-20T00:00:00.000Z", :lower_band=>177.92765761752017, :middle_band=>203.72700000000003, :upper_band=>229.5263423824799},
{:date_time=>"2018-11-19T00:00:00.000Z", :lower_band=>182.16105406114465, :middle_band=>206.0145, :upper_band=>229.86794593885534},
{:date_time=>"2018-11-16T00:00:00.000Z", :lower_band=>185.04223870650642, :middle_band=>207.75399999999996, :upper_band=>230.4657612934935},
{:date_time=>"2018-11-15T00:00:00.000Z", :lower_band=>186.80906188255153, :middle_band=>209.04299999999998, :upper_band=>231.27693811744842},
{:date_time=>"2018-11-14T00:00:00.000Z", :lower_band=>189.47053333403466, :middle_band=>210.27349999999996, :upper_band=>231.07646666596526},
{:date_time=>"2018-11-13T00:00:00.000Z", :lower_band=>193.84357681067348, :middle_band=>211.993, :upper_band=>230.1424231893265},
{:date_time=>"2018-11-12T00:00:00.000Z", :lower_band=>197.38090736241395, :middle_band=>213.48899999999998, :upper_band=>229.597092637586},
{:date_time=>"2018-11-09T00:00:00.000Z", :lower_band=>201.29218743765117, :middle_band=>214.6485, :upper_band=>228.00481256234886},
{:date_time=>"2018-11-08T00:00:00.000Z", :lower_band=>202.68427348053234, :middle_band=>215.5305, :upper_band=>228.37672651946764},
{:date_time=>"2018-11-07T00:00:00.000Z", :lower_band=>203.4002295525166, :middle_band=>215.82850000000002, :upper_band=>228.25677044748343},
{:date_time=>"2018-11-06T00:00:00.000Z", :lower_band=>204.03232701561498, :middle_band=>216.14900000000003, :upper_band=>228.26567298438508},
{:date_time=>"2018-11-05T00:00:00.000Z", :lower_band=>205.76564218562143, :middle_band=>217.30400000000003, :upper_band=>228.84235781437863}
]
expected_output = [
['2019-01-09', 141.02036711220762, 157.35499999999996, 173.6896328877923],
['2019-01-08', 141.07714470666247, 158.1695, 175.26185529333753],
['2019-01-07', 141.74551015326722, 159.05649999999997, 176.36748984673272],
['2019-01-04', 142.5717393007821, 160.39600000000002, 178.22026069921793],
['2019-01-03', 143.53956406332316, 161.8175, 180.09543593667684],
['2019-01-02', 145.3682834538487, 163.949, 182.52971654615132],
['2018-12-31', 145.53555575730587, 164.98199999999997, 184.42844424269407],
['2018-12-28', 145.90334076589886, 166.0725, 186.24165923410112],
['2018-12-27', 146.65592111904317, 167.308, 187.96007888095681],
['2018-12-26', 148.0390209273478, 168.2125, 188.38597907265222],
['2018-12-24', 149.41938426834125, 169.08499999999998, 188.7506157316587],
['2018-12-21', 153.6905118237551, 170.35799999999998, 187.02548817624486],
['2018-12-20', 157.58081627897096, 171.6605, 185.74018372102907],
['2018-12-19', 160.2737711222648, 172.66799999999998, 185.06222887773515],
['2018-12-18', 161.48722339827833, 173.91649999999998, 186.34577660172164],
['2018-12-17', 160.6411151779543, 175.28949999999995, 189.9378848220456],
['2018-12-14', 161.3586392867227, 176.66299999999998, 191.96736071327726],
['2018-12-13', 162.73753871102127, 177.72899999999998, 192.7204612889787],
['2018-12-12', 162.83769519003326, 178.79299999999998, 194.7483048099667],
['2018-12-11', 163.37450359253498, 180.04649999999998, 196.71849640746498],
['2018-12-10', 162.797082234342, 181.8385, 200.879917765658],
['2018-12-07', 162.2270311355715, 183.783, 205.33896886442847],
['2018-12-06', 162.58630667652835, 185.856, 209.12569332347164],
['2018-12-04', 163.34919566513827, 187.30850000000004, 211.2678043348618],
['2018-12-03', 164.3311203741903, 188.55350000000004, 212.77587962580978],
['2018-11-30', 164.11704019466114, 189.68650000000005, 215.25595980533896],
['2018-11-29', 163.04822623308377, 191.8685, 220.68877376691626],
['2018-11-28', 163.2435966888823, 193.83400000000003, 224.42440331111777],
['2018-11-27', 164.31484291109825, 195.45200000000006, 226.58915708890186],
['2018-11-26', 167.03813268520582, 197.35200000000003, 227.66586731479424],
['2018-11-23', 169.9836589081704, 199.436, 228.8883410918296],
['2018-11-21', 173.9574856242928, 201.81150000000002, 229.66551437570726],
['2018-11-20', 177.92765761752017, 203.72700000000003, 229.5263423824799],
['2018-11-19', 182.16105406114465, 206.0145, 229.86794593885534],
['2018-11-16', 185.04223870650642, 207.75399999999996, 230.4657612934935],
['2018-11-15', 186.80906188255153, 209.04299999999998, 231.27693811744842],
['2018-11-14', 189.47053333403466, 210.27349999999996, 231.07646666596526],
['2018-11-13', 193.84357681067348, 211.993, 230.1424231893265],
['2018-11-12', 197.38090736241395, 213.48899999999998, 229.597092637586],
['2018-11-09', 201.29218743765117, 214.6485, 228.00481256234886],
['2018-11-08', 202.68427348053234, 215.5305, 228.37672651946764],
['2018-11-07', 203.4002295525166, 215.82850000000002, 228.25677044748343],
['2018-11-06', 204.03232701561498, 216.14900000000003, 228.26567298438508],
['2018-11-05', 205.76564218562143, 217.30400000000003, 228.84235781437863]
].map { |o| { date_time: "#{o[0]}T00:00:00.000Z", lower_band: o[1], middle_band: o[2], upper_band: o[3] } }

expect(normalized_output).to eq(expected_output)
end
expect(normalized_output).to eq(expected_output)
end

it "Throws exception if not enough data" do
expect {indicator.calculate(input_data, period: input_data.size+1, price_key: :close)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError)
end
it 'throws exception if not enough data' do
expect { indicator.calculate(input_data, period: input_data.size + 1, price_key: :close) }
.to raise_exception(TechnicalAnalysis::Validation::ValidationError)
end

it 'Returns the symbol' do
indicator_symbol = indicator.indicator_symbol
expect(indicator_symbol).to eq('bb')
end
it 'returns the symbol' do
expect(indicator.indicator_symbol).to eq('bb')
end

it 'Returns the name' do
indicator_name = indicator.indicator_name
expect(indicator_name).to eq('Bollinger Bands')
end
it 'returns the name' do
expect(indicator.indicator_name).to eq('Bollinger Bands')
end

it 'Returns the valid options' do
valid_options = indicator.valid_options
expect(valid_options).to eq(%i(period standard_deviations price_key))
end
it 'returns the valid options' do
expect(indicator.valid_options).to eq(%i[period standard_deviations price_key])
end

it 'Validates options' do
valid_options = { period: 22 }
options_validated = indicator.validate_options(valid_options)
expect(options_validated).to eq(true)
end
it 'validates options' do
valid_options = { period: 22 }
expect(indicator.validate_options(valid_options)).to eq(true)
end

it 'Throws exception for invalid options' do
invalid_options = { test: 10 }
expect { indicator.validate_options(invalid_options) }.to raise_exception(TechnicalAnalysis::Validation::ValidationError)
end
it 'throws exception for invalid options' do
invalid_options = { test: 10 }
expect { indicator.validate_options(invalid_options) }.to raise_exception(TechnicalAnalysis::Validation::ValidationError)
end

it 'Calculates minimum data size' do
options = { period: 4 }
expect(indicator.min_data_size(options)).to eq(4)
it 'calculates minimum data size' do
options = { period: 4 }
expect(indicator.min_data_size(options)).to eq(4)
end

it 'performs calculations within acceptable time limit' do
time_limit = 0.5 # seconds

execution_time = Benchmark.realtime do
indicator.calculate(input_data, period: 20, standard_deviations: 2, price_key: :close)
end

expect(execution_time).to be < time_limit
end
end
end