diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..6f4f45b --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,14 @@ +version: 2 +jobs: + build: + working_directory: ~/technical-analysis + docker: + - image: circleci/ruby:2.3.6 + steps: + - checkout + + # Bundle install dependencies + - run: bundle install + + # Run the tests + - run: bundle exec rspec diff --git a/.gitignore b/.gitignore index 5e1422c..39a5d58 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ build-iPhoneSimulator/ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc +.tool-versions diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..463e822 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,37 @@ +PATH + remote: . + specs: + technical-analysis (0.2.3) + +GEM + remote: https://rubygems.org/ + specs: + diff-lcs (1.4.4) + rake (12.3.3) + rspec (3.9.0) + rspec-core (~> 3.9.0) + rspec-expectations (~> 3.9.0) + rspec-mocks (~> 3.9.0) + rspec-core (3.9.2) + rspec-support (~> 3.9.3) + rspec-expectations (3.9.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.9.0) + rspec-mocks (3.9.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.9.0) + rspec-support (3.9.3) + yard (0.9.36) + +PLATFORMS + ruby + +DEPENDENCIES + bundler (~> 1.16) + rake (~> 12.3) + rspec (~> 3.0) + technical-analysis! + yard (~> 0.9.20) + +BUNDLED WITH + 1.17.1 diff --git a/README.md b/README.md index 7e0e18c..6fd0a72 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,180 @@ -# technical-analysis +# technical-analysisfor codeland 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) +- Awesome Oscillator (AO) +- Average True Range (ATR) +- Bollinger Bands (BB) +- Commodity Channel Index (CCI) +- Chaikin Money Flow (CMF) +- Cumulative Return (CR) +- Donchian Channel (DC) +- Daily Log Return (DLR) +- Detrended Price Oscillator (DPO) +- Daily Return (DR) +- Ease of Movement (EOM) +- Exponential Moving Average (EMA) +- Force Index (FI) +- Ichimoku Kinko Hyo (ICHIMOKU) +- Keltner Channel (KC) +- Know Sure Thing (KST) +- Moving Average Convergence Divergence (MACD) +- Money Flow Index (MFI) +- Mass Index (MI) +- Negative Volume Index (NVI) +- On-balance Volume (OBV) +- On-balance Volume Mean (OBV_MEAN) +- Relative Strength Index (RSI) +- Simple Moving Average (SMA) +- Stochastic Oscillator (SR) +- Triple Exponential Average (TRIX) +- True Strength Index (TSI) +- Ultimate Oscillator (UO) +- Vortex Indicator (VI) +- Volume-price Trend (VPT) +- Volume Weighted Average Price (VWAP) +- Weighted Moving Average (WMA) +- Williams %R (WR) + +## Install + +Add the following line to Gemfile: + +```ruby +gem 'technical-analysis' +``` + +and run `bundle install` from your shell. + +To install the gem manually from your shell, run: + +```shell +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 +input_data = SpecHelper.get_test_data(:close) +# [ +# { date_time: "2019-01-09T00:00:00.000Z", close: 153.3100 }, +# { date_time: "2019-01-08T00:00:00.000Z", close: 150.7500 }, +# ... +# { date_time: "2018-10-09T00:00:00.000Z", close: 226.8700 } +# ] +``` + +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. +- `valid_options` returns an Array of keys (as Symbols) for valid options that the technical indicator accepts in its `calculate` method. +- `validate_options` returns true if the options provided are valid or raises a `ValidationError`. +- `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 +input_data = SpecHelper.get_test_data(:close) + +TechnicalAnalysis::Sma.calculate(input_data, period: 30, price_key: :close) +``` + +Here are examples of other methods for technical indicators: + +```ruby +TechnicalAnalysis::Sma.indicator_symbol +# "sma" + +TechnicalAnalysis::Sma.indicator_name +# "Simple Moving Average" + +TechnicalAnalysis::Sma.valid_options +# [:period, :price_key] + +options = { period: 30, price_key: :close } +TechnicalAnalysis::Sma.validate_options(options) +# true + +options = { period: 30, price_key: :close } +TechnicalAnalysis::Sma.min_data_size(options) +# 30 +``` + +### 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` +- The options for the indicator as a Hash - `options` + +```ruby +input_data = SpecHelper.get_test_data(:close) +options = { period: 30, price_key: :close } + +TechnicalAnalysis::Indicator.calculate('sma', input_data, :technicals, options) +``` + +Here's each example again using the generic indicator class: + +```ruby +input_data = SpecHelper.get_test_data(:close) + +TechnicalAnalysis::Indicator.calculate('sma', input_data, :indicator_symbol) +# "sma" + +TechnicalAnalysis::Indicator.calculate('sma', input_data, :indicator_name) +# "Simple Moving Average" + +TechnicalAnalysis::Indicator.calculate('sma', input_data, :valid_options) +# [:period, :price_key] + +options = { period: 30, price_key: :close } +TechnicalAnalysis::Indicator.calculate('sma', input_data, :validate_options, options) +# true + +options = { period: 30, price_key: :close } +TechnicalAnalysis::Indicator.calculate('sma', input_data, :min_data_size, options) +# 30 +``` + +Or you can use it to find the correct technical indicator class based on indicator symbol: + +```ruby +simple_moving_average = TechnicalAnalysis::Indicator.find("sma") +# TechnicalAnalysis::Sma + +input_data = SpecHelper.get_test_data(:close) +simple_moving_average.calculate(input_data, period: 30, price_key: :close) + +simple_moving_average.indicator_symbol +# "sma" + +simple_moving_average.indicator_name +# "Simple Moving Average" + +simple_moving_average.valid_options +# [:period, :price_key] + +options = { period: 30, price_key: :close } +simple_moving_average.validate_options(options) +# true + +options = { period: 30, price_key: :close } +simple_moving_average.min_data_size(options) +# 30 +``` + +## 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` \ No newline at end of file +`rspec spec` diff --git a/lib/technical_analysis.rb b/lib/technical_analysis.rb index fed0241..e6b0e41 100644 --- a/lib/technical_analysis.rb +++ b/lib/technical_analysis.rb @@ -1,8 +1,44 @@ -require "technical_analysis/calculate" - # Helpers -require 'technical_analysis/helpers/array' +require 'technical_analysis/helpers/array_helper' +require 'technical_analysis/helpers/stock_calculation' require 'technical_analysis/helpers/validation' # Indicators -require 'technical_analysis/indicators/sma' \ No newline at end of file +require 'technical_analysis/indicators/indicator' + +require 'technical_analysis/indicators/adi' +require 'technical_analysis/indicators/adtv' +require 'technical_analysis/indicators/adx' +require 'technical_analysis/indicators/ao' +require 'technical_analysis/indicators/atr' +require 'technical_analysis/indicators/bb' +require 'technical_analysis/indicators/cci' +require 'technical_analysis/indicators/cmf' +require 'technical_analysis/indicators/cr' +require 'technical_analysis/indicators/dc' +require 'technical_analysis/indicators/dlr' +require 'technical_analysis/indicators/dpo' +require 'technical_analysis/indicators/dr' +require 'technical_analysis/indicators/ema' +require 'technical_analysis/indicators/eom' +require 'technical_analysis/indicators/fi' +require 'technical_analysis/indicators/ichimoku' +require 'technical_analysis/indicators/kc' +require 'technical_analysis/indicators/kst' +require 'technical_analysis/indicators/macd' +require 'technical_analysis/indicators/mfi' +require 'technical_analysis/indicators/mi' +require 'technical_analysis/indicators/nvi' +require 'technical_analysis/indicators/obv' +require 'technical_analysis/indicators/obv_mean' +require 'technical_analysis/indicators/rsi' +require 'technical_analysis/indicators/sma' +require 'technical_analysis/indicators/sr' +require 'technical_analysis/indicators/trix' +require 'technical_analysis/indicators/tsi' +require 'technical_analysis/indicators/uo' +require 'technical_analysis/indicators/vi' +require 'technical_analysis/indicators/vpt' +require 'technical_analysis/indicators/vwap' +require 'technical_analysis/indicators/wma' +require 'technical_analysis/indicators/wr' diff --git a/lib/technical_analysis/helpers/array.rb b/lib/technical_analysis/helpers/array.rb deleted file mode 100644 index 8d60578..0000000 --- a/lib/technical_analysis/helpers/array.rb +++ /dev/null @@ -1,5 +0,0 @@ -class Array - def sum - self.inject(0, :+) - end -end \ No newline at end of file diff --git a/lib/technical_analysis/helpers/array_helper.rb b/lib/technical_analysis/helpers/array_helper.rb new file mode 100644 index 0000000..1cd5567 --- /dev/null +++ b/lib/technical_analysis/helpers/array_helper.rb @@ -0,0 +1,27 @@ +module TechnicalAnalysis + class ArrayHelper + + def self.sum(data) + data.inject(0, :+) + end + + def self.mean(data) + sum(data) / data.size.to_f + end + + def self.average(data) + sum(data) / data.size.to_f + end + + def self.sample_variance(data) + m = mean(data) + sum = data.inject(0) { |accum, i| accum + (i - m)**2 } + sum / (data.size - 1).to_f + end + + def self.standard_deviation(data) + Math.sqrt(sample_variance(data)) + end + + end +end diff --git a/lib/technical_analysis/helpers/stock_calculation.rb b/lib/technical_analysis/helpers/stock_calculation.rb new file mode 100644 index 0000000..6f1d8d2 --- /dev/null +++ b/lib/technical_analysis/helpers/stock_calculation.rb @@ -0,0 +1,33 @@ +module TechnicalAnalysis + class StockCalculation + + def self.true_range(current_high, current_low, previous_close) + [ + (current_high - current_low), + (current_high - previous_close).abs, + (current_low - previous_close).abs + ].max + end + + def self.typical_price(price) + (price[:high] + price[:low] + price[:close]) / 3.0 + end + + def self.ema(current_value, data, period, prev_value) + if prev_value.nil? + ArrayHelper.average(data) + else + (current_value - prev_value) * (2.0 / (period + 1.0)) + prev_value + end + end + + def self.wma(data) + intermediate_values = [] + data.each_with_index do |datum, i| + intermediate_values << datum * (i + 1)/(data.size * (data.size + 1)/2).to_f + end + ArrayHelper.sum(intermediate_values) + end + + end +end diff --git a/lib/technical_analysis/helpers/validation.rb b/lib/technical_analysis/helpers/validation.rb index 339fe2b..a8f8338 100644 --- a/lib/technical_analysis/helpers/validation.rb +++ b/lib/technical_analysis/helpers/validation.rb @@ -1,14 +1,33 @@ -class Validation +module TechnicalAnalysis + class Validation - def self.validate_price_data(data) - unless data.values.all? { |v| v.class == Float || v.class == Integer} - raise ValidationError.new "Invalid Data. Price is not a number" + def self.validate_numeric_data(data, *keys) + keys.each do |key| + unless data.all? { |v| v[key].is_a? Numeric } + raise ValidationError.new "Invalid Data. '#{key}' is not valid price data." + end + end end - end - def self.validate_length(data, size) - raise ValidationError.new "Not enough data for that period" if data.size < size - end + def self.validate_length(data, size) + raise ValidationError.new "Not enough data for that period" if data.size < size + end + + def self.validate_options(options, valid_options) + raise ValidationError.new "Options must be a hash." unless options.respond_to? :keys + raise ValidationError.new "No valid options provided." unless valid_options + + return true if (options.keys - valid_options).empty? + raise ValidationError.new "Invalid options provided. Valid options are #{valid_options.join(", ")}" + end + + def self.validate_date_time_key(data, date_time_key=:date_time) + unless data.all? { |row| row.keys.include? date_time_key } + raise ValidationError.new "Dataset must include '#{date_time_key}' field with timestamps" + end + end - class ValidationError < StandardError; end -end \ No newline at end of file + class ValidationError < StandardError; end + + end +end diff --git a/lib/technical_analysis/indicators/adi.rb b/lib/technical_analysis/indicators/adi.rb new file mode 100644 index 0000000..198be86 --- /dev/null +++ b/lib/technical_analysis/indicators/adi.rb @@ -0,0 +1,101 @@ +module TechnicalAnalysis + # Accumulation/Distribution Index + class Adi < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "adi" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Accumulation/Distribution Index" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + [] + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + return true if options == {} + raise Validation::ValidationError.new "This indicator doesn't accept any options." + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(**params) + 1 + end + + # Calculates the Accumulation/Distribution Index (ADI) for the given data + # https://en.wikipedia.org/wiki/Accumulation/distribution_index + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low, :close, :volume) + # @return [Array] An array of AdiValue instances + def self.calculate(data) + Validation.validate_numeric_data(data, :high, :low, :close, :volume) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + ad = 0 + clv = 0 + output = [] + prev_ad = 0 + + data.each_with_index do |values, i| + if values[:high] == values[:low] + clv = 0 + else + clv = ((values[:close] - values[:low]) - (values[:high] - values[:close])) / (values[:high] - values[:low]) + end + + ad = prev_ad + (clv * values[:volume]) + prev_ad = ad + date_time = values[:date_time] + + output << AdiValue.new(date_time: date_time, adi: ad) + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class AdiValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the adi calculation value + attr_accessor :adi + + def initialize(date_time: nil, adi: nil) + @date_time = date_time + @adi = adi + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, adi: @adi } + end + + end +end diff --git a/lib/technical_analysis/indicators/adtv.rb b/lib/technical_analysis/indicators/adtv.rb new file mode 100644 index 0000000..de0fdb9 --- /dev/null +++ b/lib/technical_analysis/indicators/adtv.rb @@ -0,0 +1,98 @@ +module TechnicalAnalysis + # Average Daily Trading Volume + class Adtv < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "adtv" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Average Daily Trading Volume" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period volume_key) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 22, **params) + period.to_i + end + + # Calculates the average daily trading volume (ADTV) for the data over the given period + # https://www.investopedia.com/terms/a/averagedailytradingvolume.asp + # + # @param data [Array] Array of hashes with keys (:date_time, :value) + # @param period [Integer] The given number of days used to calculate the ADTV + # @param volume_key [Symbol] The hash key for the volume data. Default :value + # + # @return [Array] An array of AdtvValue instances + def self.calculate(data, period: 22, volume_key: :value) + period = period.to_i + volume_key = volume_key.to_sym + Validation.validate_numeric_data(data, volume_key) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + period_values = [] + + data.each do |v| + period_values << v[volume_key] + if period_values.size == period + output << AdtvValue.new(date_time: v[:date_time], adtv: ArrayHelper.average(period_values)) + period_values.shift + end + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class AdtvValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the adtv calculation value + attr_accessor :adtv + + def initialize(date_time: nil, adtv: nil) + @date_time = date_time + @adtv = adtv + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, adtv: @adtv } + end + + end +end diff --git a/lib/technical_analysis/indicators/adx.rb b/lib/technical_analysis/indicators/adx.rb new file mode 100644 index 0000000..abe8506 --- /dev/null +++ b/lib/technical_analysis/indicators/adx.rb @@ -0,0 +1,168 @@ +module TechnicalAnalysis + # Average Direcitonal Index + class Adx < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "adx" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Average Directional Index" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 14) + period.to_i * 2 + end + + # Calculates the average directional index (ADX) for the data over the given period + # https://en.wikipedia.org/wiki/Average_directional_movement_index + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low, :close) + # @param period [Integer] The given period to calculate the ADX + # + # @return [Array] An array of AdxValue instances + def self.calculate(data, period: 14) + period = period.to_i + Validation.validate_numeric_data(data, :high, :low, :close) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + dx_values = [] + output = [] + periodic_values = [] + prev_adx = nil + prev_price = data.shift + smoothed_values = [] + + data.each do |v| + tr = StockCalculation.true_range(v[:high], v[:low], prev_price[:close]) + + dm_pos, dm_neg = calculate_dm(v, prev_price) + + periodic_values << { tr: tr, dm_pos: dm_pos, dm_neg: dm_neg } + + if periodic_values.size == period + tr_period, dm_pos_period, dm_neg_period = smooth_periodic_values(period, periodic_values, smoothed_values) + smoothed_values << { tr: tr_period, dm_pos: dm_pos_period, dm_neg: dm_neg_period } + + di_pos = (dm_pos_period / tr_period) * 100.00 + di_neg = (dm_neg_period / tr_period) * 100.00 + dx = ((dm_pos_period - dm_neg_period).abs / (dm_pos_period + dm_neg_period) * 100.00) + + dx_values << dx + + if dx_values.size == period + if prev_adx.nil? + adx = ArrayHelper.average(dx_values) + else + adx = ((prev_adx * (period - 1)) + dx) / period.to_f + end + + output << AdxValue.new(date_time: v[:date_time], adx: adx, di_pos: di_pos, di_neg: di_neg) + + prev_adx = adx + dx_values.shift + end + + periodic_values.shift + end + + prev_price = v + end + + output.sort_by(&:date_time).reverse + end + + private_class_method def self.calculate_dm(current_price, prev_price) + if current_price[:high] - prev_price[:high] > prev_price[:low] - current_price[:low] + dm_pos = [(current_price[:high] - prev_price[:high]), 0].max + dm_neg = 0 + elsif prev_price[:low] - current_price[:low] > current_price[:high] - prev_price[:high] + dm_pos = 0 + dm_neg = [(prev_price[:low] - current_price[:low]), 0].max + else + dm_pos = 0 + dm_neg = 0 + end + + [dm_pos, dm_neg] + end + + private_class_method def self.smooth_periodic_values(period, periodic_values, smoothed_values) + if smoothed_values.empty? + tr_period = ArrayHelper.sum(periodic_values.map { |pv| pv[:tr] }) + dm_pos_period = ArrayHelper.sum(periodic_values.map { |pv| pv[:dm_pos] }) + dm_neg_period = ArrayHelper.sum(periodic_values.map { |pv| pv[:dm_neg] }) + else + prev_value = smoothed_values.last + current_value = periodic_values.last + + tr_period = prev_value[:tr] - (prev_value[:tr] / period) + current_value[:tr] + dm_pos_period = prev_value[:dm_pos] - (prev_value[:dm_pos] / period) + current_value[:dm_pos] + dm_neg_period = prev_value[:dm_neg] - (prev_value[:dm_neg] / period) + current_value[:dm_neg] + end + + [tr_period, dm_pos_period, dm_neg_period] + end + + # The value class to be returned by calculations + class AdxValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the adx calculation value + attr_accessor :adx + + # @return [Float] the positive directional indicator calculation value + attr_accessor :di_pos + + # @return [Float] the negative directional indicator calculation value + attr_accessor :di_neg + + def initialize(date_time: nil, adx: nil, di_pos: nil, di_neg: nil) + @date_time = date_time + @adx = adx + @di_pos = di_pos + @di_neg = di_neg + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, adx: @adx, di_pos: @di_pos, di_neg: @di_neg } + end + + end + + end +end diff --git a/lib/technical_analysis/indicators/ao.rb b/lib/technical_analysis/indicators/ao.rb new file mode 100644 index 0000000..3ca8fbd --- /dev/null +++ b/lib/technical_analysis/indicators/ao.rb @@ -0,0 +1,105 @@ +module TechnicalAnalysis + # Awesome Oscillator + class Ao < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "ao" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Awesome Oscillator" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(short_period long_period) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(long_period: 34, **params) + long_period.to_i + end + + # Calculates the awesome oscillator for the data over the given period + # https://www.tradingview.com/wiki/Awesome_Oscillator_(AO) + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low) + # @param short_period [Integer] The given period to calculate the short period SMA + # @param long_period [Integer] The given period to calculate the long period SMA + # + # @return [Array] An array of AoValue instances + def self.calculate(data, short_period: 5, long_period: 34) + short_period = short_period.to_i + long_period = long_period.to_i + Validation.validate_numeric_data(data, :high, :low) + Validation.validate_length(data, min_data_size(long_period: long_period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + midpoint_values = [] + output = [] + + data.each do |v| + midpoint = (v[:high] + v[:low]) / 2 + midpoint_values << midpoint + + if midpoint_values.size == long_period + short_period_sma = ArrayHelper.average(midpoint_values.last(short_period)) + long_period_sma = ArrayHelper.average(midpoint_values) + value = short_period_sma - long_period_sma + + output << AoValue.new(date_time: v[:date_time], ao: value) + + midpoint_values.shift + end + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class AoValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the ao calculation value + attr_accessor :ao + + def initialize(date_time: nil, ao: nil) + @date_time = date_time + @ao = ao + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, ao: @ao } + end + + end +end diff --git a/lib/technical_analysis/indicators/atr.rb b/lib/technical_analysis/indicators/atr.rb new file mode 100644 index 0000000..fc47494 --- /dev/null +++ b/lib/technical_analysis/indicators/atr.rb @@ -0,0 +1,109 @@ +module TechnicalAnalysis + # Average True Range + class Atr < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "atr" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Average True Range" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 14) + period.to_i + 1 + end + + # Calculates the average true range (ATR) for the data over the given period + # https://en.wikipedia.org/wiki/Average_true_range + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low, :close) + # @param period [Integer] The given period to calculate the ATR + # + # @return [Array] An array of AtrValue instances + def self.calculate(data, period: 14) + period = period.to_i + Validation.validate_numeric_data(data, :high, :low, :close) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + period_values = [] + prev_price = data.shift + + data.each do |v| + tr = StockCalculation.true_range(v[:high], v[:low], prev_price[:close]) + + period_values << tr + + if period_values.size == period + if output.empty? + atr = ArrayHelper.average(period_values) + else + atr = (output.last.atr * (period - 1.0) + tr) / period.to_f + end + + output << AtrValue.new(date_time: v[:date_time], atr: atr) + + period_values.shift + end + + prev_price = v + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class AtrValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the atr calculation value + attr_accessor :atr + + def initialize(date_time: nil, atr: nil) + @date_time = date_time + @atr = atr + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, atr: @atr } + end + + end +end diff --git a/lib/technical_analysis/indicators/bb.rb b/lib/technical_analysis/indicators/bb.rb new file mode 100644 index 0000000..3897667 --- /dev/null +++ b/lib/technical_analysis/indicators/bb.rb @@ -0,0 +1,126 @@ +module TechnicalAnalysis + # Bollinger Bands + class Bb < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "bb" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Bollinger Bands" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period standard_deviations price_key) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 20, **params) + period.to_i + end + + # Calculates the bollinger bands (BB) for the data over the given period + # https://en.wikipedia.org/wiki/Bollinger_Bands + # + # @param data [Array] Array of hashes with keys (:date_time, :value) + # @param period [Integer] The given period to calculate the BB + # @param standard_deviations [Float] The given standard deviations to calculate the upper and + # lower bands of the BB + # @param price_key [Symbol] The hash key for the price data. Default :value + # + # @return [Array] An array of BbValue instances + def self.calculate(data, period: 20, standard_deviations: 2, price_key: :value) + period = period.to_i + standard_deviations = standard_deviations.to_f + price_key = price_key.to_sym + Validation.validate_numeric_data(data, price_key) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + period_values = [] + + data.each do |v| + period_values << v[price_key] + + if period_values.size == period + mb = ArrayHelper.average(period_values) + sd = ArrayHelper.standard_deviation(period_values) + ub = mb + standard_deviations * sd + lb = mb - standard_deviations * sd + + output << BbValue.new( + date_time: v[:date_time], + lower_band: lb, + middle_band: mb, + upper_band: ub + ) + + period_values.shift + end + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class BbValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the lower_band calculation value + attr_accessor :lower_band + + # @return [Float] the middle_band calculation value + attr_accessor :middle_band + + # @return [Float] the upper_band calculation value + attr_accessor :upper_band + + def initialize(date_time: nil, lower_band: nil, middle_band: nil, upper_band: nil) + @date_time = date_time + @lower_band = lower_band + @middle_band = middle_band + @upper_band = upper_band + end + + # @return [Hash] the attributes as a hash + def to_hash + { + date_time: @date_time, + lower_band: @lower_band, + middle_band: @middle_band, + upper_band: @upper_band + } + end + + end +end diff --git a/lib/technical_analysis/indicators/cci.rb b/lib/technical_analysis/indicators/cci.rb new file mode 100644 index 0000000..75f6b7b --- /dev/null +++ b/lib/technical_analysis/indicators/cci.rb @@ -0,0 +1,105 @@ +module TechnicalAnalysis + class Cci < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "cci" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Commodity Channel Index" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period constant) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 20, **params) + period.to_i + end + + # Calculates the commodity channel index (CCI) for the data over the given period + # https://en.wikipedia.org/wiki/Commodity_channel_index + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low, :close) + # @param period [Integer] The given period to calculate the CCI + # @param constant [Float] The given constant to ensure that approximately 70 to 80 percent of + # CCI values would fall between −100 and +100 + # + # @return [Array] An array of CciValue instances + def self.calculate(data, period: 20, constant: 0.015) + period = period.to_i + constant = constant.to_f + Validation.validate_numeric_data(data, :high, :low, :close) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + typical_prices = [] + + data.each do |v| + typical_price = StockCalculation.typical_price(v) + typical_prices << typical_price + + if typical_prices.size == period + period_sma = ArrayHelper.average(typical_prices) + mean_deviation = ArrayHelper.mean(typical_prices.map { |tp| (tp - period_sma).abs }) + cci = (typical_price - period_sma) / (constant * mean_deviation) + + output << CciValue.new(date_time: v[:date_time], cci: cci) + + typical_prices.shift + end + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class CciValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the cci calculation value + attr_accessor :cci + + def initialize(date_time: nil, cci: nil) + @date_time = date_time + @cci = cci + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, cci: @cci } + end + + end +end diff --git a/lib/technical_analysis/indicators/cmf.rb b/lib/technical_analysis/indicators/cmf.rb new file mode 100644 index 0000000..683f7e8 --- /dev/null +++ b/lib/technical_analysis/indicators/cmf.rb @@ -0,0 +1,105 @@ +module TechnicalAnalysis + # Chaikin Money Flow + class Cmf < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "cmf" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Chaikin Money Flow" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 20) + period.to_i + end + + # Calculates the chaikin money flow (CMF) for the data over the given period + # https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:chaikin_money_flow_cmf + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low, :close, :volume) + # @param period [Integer] The given period to calculate the CMF + # + # @return [Array] An array of CmfValue instances + def self.calculate(data, period: 20) + period = period.to_i + Validation.validate_numeric_data(data, :high, :low, :close, :volume) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + period_values = [] + + data.each do |v| + multiplier = ((v[:close] - v[:low]) - (v[:high] - v[:close])) / (v[:high] - v[:low]) + mf_volume = multiplier * v[:volume] + + period_values << { volume: v[:volume], mf_volume: mf_volume } + + if period_values.size == period + volume_sum = ArrayHelper.sum(period_values.map { |pv| pv[:volume] }) + mf_volume_sum = ArrayHelper.sum(period_values.map { |pv| pv[:mf_volume] }) + cmf = mf_volume_sum / volume_sum + + output << CmfValue.new(date_time: v[:date_time], cmf: cmf) + + period_values.shift + end + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class CmfValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the cmf calculation value + attr_accessor :cmf + + def initialize(date_time: nil, cmf: nil) + @date_time = date_time + @cmf = cmf + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, cmf: @cmf } + end + + end +end diff --git a/lib/technical_analysis/indicators/cr.rb b/lib/technical_analysis/indicators/cr.rb new file mode 100644 index 0000000..55fcc14 --- /dev/null +++ b/lib/technical_analysis/indicators/cr.rb @@ -0,0 +1,95 @@ +module TechnicalAnalysis + # Cumulative Return + class Cr < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "cr" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Cumulative Return" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(price_key) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(**params) + 1 + end + + # Calculates the cumulative return (CR) for the data over the given period + # https://www.investopedia.com/terms/c/cumulativereturn.asp + # + # @param data [Array] Array of hashes with keys (:date_time, :value) + # @param price_key [Symbol] The hash key for the price data. Default :value + # + # @return [Array] An array of CrValue instances + def self.calculate(data, price_key: :value) + price_key = price_key.to_sym + Validation.validate_numeric_data(data, price_key) + Validation.validate_length(data, min_data_size({})) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + start_price = data.first[price_key] + + data.each do |v| + output << CrValue.new( + date_time: v[:date_time], + cr: ((v[price_key] - start_price) / start_price) + ) + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class CrValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the cr calculation value + attr_accessor :cr + + def initialize(date_time: nil, cr: nil) + @date_time = date_time + @cr = cr + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, cr: @cr } + end + + end +end diff --git a/lib/technical_analysis/indicators/dc.rb b/lib/technical_analysis/indicators/dc.rb new file mode 100644 index 0000000..1af1712 --- /dev/null +++ b/lib/technical_analysis/indicators/dc.rb @@ -0,0 +1,108 @@ +module TechnicalAnalysis + # Donchian Channel + class Dc < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "dc" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Donchian Channel" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period price_key) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 20, **params) + period.to_i + end + + # Calculates the donchian channel (DC) for the data over the given period + # https://en.wikipedia.org/wiki/Donchian_channel + # + # @param data [Array] Array of hashes with keys (:date_time, :value) + # @param period [Integer] The given period to calculate the DC + # @param price_key [Symbol] The hash key for the price data. Default :value + # + # @return [Array] An array of DcValue instances + def self.calculate(data, period: 20, price_key: :value) + period = period.to_i + price_key = price_key.to_sym + Validation.validate_numeric_data(data, price_key) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + period_values = [] + + data.each do |v| + period_values << v[price_key] + + if period_values.size == period + output << DcValue.new( + date_time: v[:date_time], + upper_bound: period_values.max, + lower_bound: period_values.min + ) + + period_values.shift + end + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class DcValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the upper_bound calculation value + attr_accessor :upper_bound + + # @return [Float] the lower_bound calculation value + attr_accessor :lower_bound + + def initialize(date_time: nil, upper_bound: nil, lower_bound: ninl) + @date_time = date_time + @upper_bound = upper_bound + @lower_bound = lower_bound + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, upper_bound: @upper_bound, lower_bound: @lower_bound } + end + + end +end diff --git a/lib/technical_analysis/indicators/dlr.rb b/lib/technical_analysis/indicators/dlr.rb new file mode 100644 index 0000000..14569af --- /dev/null +++ b/lib/technical_analysis/indicators/dlr.rb @@ -0,0 +1,97 @@ +module TechnicalAnalysis + # Daily Log Return + class Dlr < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "dlr" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Daily Log Return" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(price_key) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(**params) + 1 + end + + # Calculates the daily log return (percent expressed as a decimal) for the data over the given period + # https://www.quora.com/What-are-daily-log-returns-of-an-equity + # https://en.wikipedia.org/wiki/Rate_of_return#Logarithmic_or_continuously_compounded_return + # + # @param data [Array] Array of hashes with keys (:date_time, :value) + # @param price_key [Symbol] The hash key for the price data. Default :value + # + # @return [Array] An array of DlrValue instances + def self.calculate(data, price_key: :value) + price_key = price_key.to_sym + Validation.validate_numeric_data(data, price_key) + Validation.validate_length(data, min_data_size({})) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + prev_price = data.first[price_key].to_f + + data.each do |v| + current_price = v[:close].to_f + + output << DlrValue.new(date_time: v[:date_time], dlr: Math.log(current_price / prev_price)) + + prev_price = current_price + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class DlrValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the dlr calculation value + attr_accessor :dlr + + def initialize(date_time: nil, dlr: nil) + @date_time = date_time + @dlr = dlr + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, dlr: @dlr } + end + + end +end diff --git a/lib/technical_analysis/indicators/dpo.rb b/lib/technical_analysis/indicators/dpo.rb new file mode 100644 index 0000000..b1fdf84 --- /dev/null +++ b/lib/technical_analysis/indicators/dpo.rb @@ -0,0 +1,106 @@ +module TechnicalAnalysis + # Detrended Price Oscillator + class Dpo < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "dpo" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Detrended Price Oscillator" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period price_key) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 20, **params) + period.to_i + (period.to_i / 2) + end + + # Calculates the detrended price oscillator for the data over the given period + # https://en.wikipedia.org/wiki/Detrended_price_oscillator + # + # @param data [Array] Array of hashes with keys (:date_time, :value) + # @param period [Integer] The given period to calculate the SMA + # @param price_key [Symbol] The hash key for the price data. Default :value + # + # @return [Array] + def self.calculate(data, period: 20, price_key: :value) + period = period.to_i + price_key = price_key.to_sym + Validation.validate_numeric_data(data, price_key) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + index = period + (period / 2) - 1 + midpoint_index = (period / 2) + 1 + output = [] + + while index < data.size + current_record = data[index] + date_time = current_record[:date_time] + current_price = current_record[price_key] + + sma_range = (index - midpoint_index - period + 2)..(index - midpoint_index + 1) + midpoint_period_sma = ArrayHelper.average(data[sma_range].map { |v| v[price_key] }) + + dpo = (current_price - midpoint_period_sma) + + output << DpoValue.new(date_time: date_time, dpo: dpo) + + index += 1 + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class DpoValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the dpo calculation value + attr_accessor :dpo + + def initialize(date_time: nil, dpo: nil) + @date_time = date_time + @dpo = dpo + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, dpo: @dpo } + end + + end +end diff --git a/lib/technical_analysis/indicators/dr.rb b/lib/technical_analysis/indicators/dr.rb new file mode 100644 index 0000000..2be3d13 --- /dev/null +++ b/lib/technical_analysis/indicators/dr.rb @@ -0,0 +1,96 @@ +module TechnicalAnalysis + # Daily Return + class Dr < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "dr" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Daily Return" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(price_key) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(**params) + 1 + end + + # Calculates the daily return (percent expressed as a decimal) for the data over the given period + # https://en.wikipedia.org/wiki/Rate_of_return + # + # @param data [Array] Array of hashes with keys (:date_time, :value) + # @param price_key [Symbol] The hash key for the price data. Default :value + # + # @return [Array] An array of DrValue instances + def self.calculate(data, price_key: :value) + price_key = price_key.to_sym + Validation.validate_numeric_data(data, price_key) + Validation.validate_length(data, min_data_size({})) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + prev_price = data.first[price_key].to_f + + data.each do |v| + current_price = v[:close].to_f + + output << DrValue.new(date_time: v[:date_time], dr: ((current_price / prev_price) - 1)) + + prev_price = current_price + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class DrValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the dr calculation value + attr_accessor :dr + + def initialize(date_time: nil, dr: nil) + @date_time = date_time + @dr = dr + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, dr: @dr } + end + + end +end diff --git a/lib/technical_analysis/indicators/ema.rb b/lib/technical_analysis/indicators/ema.rb new file mode 100644 index 0000000..425c3b1 --- /dev/null +++ b/lib/technical_analysis/indicators/ema.rb @@ -0,0 +1,103 @@ +module TechnicalAnalysis + # Exponential Moving Average + class Ema < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "ema" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Exponential Moving Average" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period price_key date_time_key) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 30, **params) + period.to_i + end + + # Calculates the exponential moving average (EMA) for the data over the given period + # https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average + # + # @param data [Array] Array of hashes with keys (:date_time, :value) + # @param period [Integer] The given period to calculate the EMA + # @param price_key [Symbol] The hash key for the price data. Default :value + # @param date_time_key [Symbol] The hash key for the date time data. Default :date_time + # + # @return [Array] An array of EmaValue instances + def self.calculate(data, period: 30, price_key: :value, date_time_key: :date_time) + period = period.to_i + price_key = price_key.to_sym + date_time_key = date_time_key.to_sym + Validation.validate_numeric_data(data, price_key) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data, date_time_key) + + data = data.sort_by { |row| row[date_time_key] } + + output = [] + period_values = [] + previous_ema = nil + + data.each do |v| + period_values << v[price_key] + if period_values.size == period + ema = StockCalculation.ema(v[price_key], period_values, period, previous_ema) + previous_ema = ema + + output << EmaValue.new(date_time: v[date_time_key], ema: ema) + period_values.shift + end + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class EmaValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the ema calculation value + attr_accessor :ema + + def initialize(date_time: nil, ema: nil) + @date_time = date_time + @ema = ema + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, ema: @ema } + end + end +end diff --git a/lib/technical_analysis/indicators/eom.rb b/lib/technical_analysis/indicators/eom.rb new file mode 100644 index 0000000..e46f8f8 --- /dev/null +++ b/lib/technical_analysis/indicators/eom.rb @@ -0,0 +1,104 @@ +module TechnicalAnalysis + # Ease of Movement + class Eom < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "eom" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Ease of Movement" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 14) + period.to_i + 1 + end + + # Calculates the ease of movement (EoM) for the data over the given period + # https://en.wikipedia.org/wiki/Ease_of_movement + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low, :volume) + # @param period [Integer] The given period to calculate the EoM + # + # @return [Array] An array of EomValue instances + def self.calculate(data, period: 14) + period = period.to_i + Validation.validate_numeric_data(data, :high, :low, :volume) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + period_values = [] + prev_price = data.shift + + data.each do |v| + distance_moved = ((v[:high] + v[:low]) / 2) - ((prev_price[:high] + prev_price[:low]) / 2) + box_ratio = (v[:volume] / 100_000_000.00) / (v[:high] - v[:low]) + emv = distance_moved / box_ratio + + period_values << emv + + if period_values.size == period + output << EomValue.new(date_time: v[:date_time], eom: ArrayHelper.average(period_values)) + period_values.shift + end + + prev_price = v + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class EomValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the eom calculation value + attr_accessor :eom + + def initialize(date_time: nil, eom: nil) + @date_time = date_time + @eom = eom + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, eom: @eom } + end + + end +end diff --git a/lib/technical_analysis/indicators/fi.rb b/lib/technical_analysis/indicators/fi.rb new file mode 100644 index 0000000..6587230 --- /dev/null +++ b/lib/technical_analysis/indicators/fi.rb @@ -0,0 +1,95 @@ +module TechnicalAnalysis + # Force Index + class Fi < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "fi" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Force Index" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + [] + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + return true if options == {} + raise Validation::ValidationError.new "This indicator doesn't accept any options." + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(**params) + 2 + end + + # Calculates the force index (FI) for the data + # https://en.wikipedia.org/wiki/Force_index + # + # @param data [Array] Array of hashes with keys (:date_time, :close, :volume) + # + # @return [Array] An array of FiValue instances + def self.calculate(data) + Validation.validate_numeric_data(data, :close, :volume) + Validation.validate_length(data, min_data_size({})) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + prev_price = data.shift + + data.each do |v| + fi = ((v[:close] - prev_price[:close]) * v[:volume]) + + output << FiValue.new(date_time: v[:date_time], fi: fi) + + prev_price = v + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class FiValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the fi calculation value + attr_accessor :fi + + def initialize(date_time: nil, fi: nil) + @date_time = date_time + @fi = fi + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, fi: @fi } + end + + end +end diff --git a/lib/technical_analysis/indicators/ichimoku.rb b/lib/technical_analysis/indicators/ichimoku.rb new file mode 100644 index 0000000..a211098 --- /dev/null +++ b/lib/technical_analysis/indicators/ichimoku.rb @@ -0,0 +1,179 @@ +module TechnicalAnalysis + # Ichimoku Kinko Hyo + class Ichimoku < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "ichimoku" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Ichimoku Kinko Hyo" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(low_period medium_period high_period) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(medium_period: 26, high_period: 52, **params) + high_period.to_i + medium_period.to_i - 1 + end + + # Calculates the 5 points of Ichimoku Kinko Hyo (Ichimoku) for the data over the given period + # 1. tenkan_sen (Conversion Line) + # 2. kijun_sen (Base Line) + # 3. senkou_span_a (Leading Span A) + # 4. senkou_span_b (Leading Span B) + # 5. chickou_span (Lagging Span) + # https://en.wikipedia.org/wiki/Ichimoku_Kink%C5%8D_Hy%C5%8D + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low, :close) + # @param low_period [Integer] The given period to calculate tenkan_sen (Conversion Line) + # @param medium_period [Integer] The given period to calculate kijun_sen (Base Line), senkou_span_a (Leading Span A), and chikou_span (Lagging Span) + # @param high_period [Integer] The given period to calculate senkou_span_b (Leading Span B) + # + # @return [Array] An array of IchimokuValue instances + def self.calculate(data, low_period: 9, medium_period: 26, high_period: 52) + low_period = low_period.to_i + medium_period = medium_period.to_i + high_period = high_period.to_i + Validation.validate_numeric_data(data, :high, :low, :close) + Validation.validate_length(data, min_data_size(high_period: high_period, medium_period: medium_period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + index = high_period + medium_period - 2 + output = [] + + while index < data.size + date_time = data[index][:date_time] + + tenkan_sen = calculate_midpoint(index, low_period, data) + kinjun_sen = calculate_midpoint(index, medium_period, data) + senkou_span_a = calculate_senkou_span_a(index, low_period, medium_period, data) + senkou_span_b = calculate_senkou_span_b(index, medium_period, high_period, data) + chikou_span = calculate_chikou_span(index, medium_period, data) + + output << IchimokuValue.new( + date_time: date_time, + tenkan_sen: tenkan_sen, + kijun_sen: kinjun_sen, + senkou_span_a: senkou_span_a, + senkou_span_b: senkou_span_b, + chikou_span: chikou_span + ) + + index += 1 + end + + output.sort_by(&:date_time).reverse + end + + private_class_method def self.lowest_low(prices) + prices.map { |price| price[:low] }.min + end + + private_class_method def self.highest_high(prices) + prices.map { |price| price[:high] }.max + end + + private_class_method def self.calculate_midpoint(index, period, data) + period_range = ((index - (period - 1))..index) + period_data = data[period_range] + lowest_low = lowest_low(period_data) + highest_high = highest_high(period_data) + + ((highest_high + lowest_low) / 2.0) + end + + private_class_method def self.calculate_senkou_span_a(index, low_period, medium_period, data) + mp_ago_index = (index - (medium_period - 1)) + + tenkan_sen_mp_ago = calculate_midpoint(mp_ago_index, low_period, data) + kinjun_sen_mp_ago = calculate_midpoint(mp_ago_index, medium_period, data) + + ((tenkan_sen_mp_ago + kinjun_sen_mp_ago) / 2.0) + end + + private_class_method def self.calculate_senkou_span_b(index, medium_period, high_period, data) + mp_ago_index = (index - (medium_period - 1)) + + calculate_midpoint(mp_ago_index, high_period, data) + end + + private_class_method def self.calculate_chikou_span(index, medium_period, data) + mp_ago_index = (index - (medium_period - 1)) + + data[mp_ago_index][:close] + end + + end + + # The value class to be returned by calculations + class IchimokuValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the tenkan_sen calculation value + attr_accessor :tenkan_sen + + # @return [Float] the kijun_sen calculation value + attr_accessor :kijun_sen + + # @return [Float] the senkou_span_a calculation value + attr_accessor :senkou_span_a + + # @return [Float] the senkou_span_b calculation value + attr_accessor :senkou_span_b + + # @return [Float] the chikou_span calculation value + attr_accessor :chikou_span + + def initialize(date_time: nil, tenkan_sen: nil, kijun_sen: nil, senkou_span_a: nil, senkou_span_b: nil, chikou_span: nil) + @date_time = date_time + @tenkan_sen = tenkan_sen + @kijun_sen = kijun_sen + @senkou_span_a = senkou_span_a + @senkou_span_b = senkou_span_b + @chikou_span = chikou_span + end + + # @return [Hash] the attributes as a hash + def to_hash + { + date_time: @date_time, + tenkan_sen: @tenkan_sen, + kijun_sen: @kijun_sen, + senkou_span_a: @senkou_span_a, + senkou_span_b: @senkou_span_b, + chikou_span: @chikou_span + } + end + + end +end diff --git a/lib/technical_analysis/indicators/indicator.rb b/lib/technical_analysis/indicators/indicator.rb new file mode 100644 index 0000000..f518b80 --- /dev/null +++ b/lib/technical_analysis/indicators/indicator.rb @@ -0,0 +1,178 @@ +module TechnicalAnalysis + class Indicator + + CALCULATIONS = [ + :indicator_name, + :indicator_symbol, + :min_data_size, + :technicals, + :valid_options, + :validate_options, + ].freeze + + private_constant :CALCULATIONS + + # Returns an array of TechnicalAnalysis modules + # + # @return [Array] A list of TechnicalAnalysis::Class + def self.roster + [ + TechnicalAnalysis::Adi, + TechnicalAnalysis::Adtv, + TechnicalAnalysis::Adx, + TechnicalAnalysis::Ao, + TechnicalAnalysis::Atr, + TechnicalAnalysis::Bb, + TechnicalAnalysis::Cci, + TechnicalAnalysis::Cmf, + TechnicalAnalysis::Cr, + TechnicalAnalysis::Dc, + TechnicalAnalysis::Dlr, + TechnicalAnalysis::Dpo, + TechnicalAnalysis::Dr, + TechnicalAnalysis::Eom, + TechnicalAnalysis::Fi, + TechnicalAnalysis::Ichimoku, + TechnicalAnalysis::Kc, + TechnicalAnalysis::Kst, + TechnicalAnalysis::Macd, + TechnicalAnalysis::Mfi, + TechnicalAnalysis::Mi, + TechnicalAnalysis::Nvi, + TechnicalAnalysis::Obv, + TechnicalAnalysis::ObvMean, + TechnicalAnalysis::Rsi, + TechnicalAnalysis::Sma, + TechnicalAnalysis::Sr, + TechnicalAnalysis::Trix, + TechnicalAnalysis::Tsi, + TechnicalAnalysis::Uo, + TechnicalAnalysis::Vi, + TechnicalAnalysis::Vpt, + TechnicalAnalysis::Vwap, + TechnicalAnalysis::Wr, + ] + end + + def self.roster_hash + { + TechnicalAnalysis::Adi.indicator_symbol => TechnicalAnalysis::Adi, + TechnicalAnalysis::Adtv.indicator_symbol => TechnicalAnalysis::Adtv, + TechnicalAnalysis::Adx.indicator_symbol => TechnicalAnalysis::Adx, + TechnicalAnalysis::Ao.indicator_symbol => TechnicalAnalysis::Ao, + TechnicalAnalysis::Atr.indicator_symbol => TechnicalAnalysis::Atr, + TechnicalAnalysis::Bb.indicator_symbol => TechnicalAnalysis::Bb, + TechnicalAnalysis::Cci.indicator_symbol => TechnicalAnalysis::Cci, + TechnicalAnalysis::Cmf.indicator_symbol => TechnicalAnalysis::Cmf, + TechnicalAnalysis::Cr.indicator_symbol => TechnicalAnalysis::Cr, + TechnicalAnalysis::Dc.indicator_symbol => TechnicalAnalysis::Dc, + TechnicalAnalysis::Dlr.indicator_symbol => TechnicalAnalysis::Dlr, + TechnicalAnalysis::Dpo.indicator_symbol => TechnicalAnalysis::Dpo, + TechnicalAnalysis::Dr.indicator_symbol => TechnicalAnalysis::Dr, + TechnicalAnalysis::Eom.indicator_symbol => TechnicalAnalysis::Eom, + TechnicalAnalysis::Fi.indicator_symbol => TechnicalAnalysis::Fi, + TechnicalAnalysis::Ichimoku.indicator_symbol => TechnicalAnalysis::Ichimoku, + TechnicalAnalysis::Kc.indicator_symbol => TechnicalAnalysis::Kc, + TechnicalAnalysis::Kst.indicator_symbol => TechnicalAnalysis::Kst, + TechnicalAnalysis::Macd.indicator_symbol => TechnicalAnalysis::Macd, + TechnicalAnalysis::Mfi.indicator_symbol => TechnicalAnalysis::Mfi, + TechnicalAnalysis::Mi.indicator_symbol => TechnicalAnalysis::Mi, + TechnicalAnalysis::Nvi.indicator_symbol => TechnicalAnalysis::Nvi, + TechnicalAnalysis::Obv.indicator_symbol => TechnicalAnalysis::Obv, + TechnicalAnalysis::ObvMean.indicator_symbol => TechnicalAnalysis::ObvMean, + TechnicalAnalysis::Rsi.indicator_symbol => TechnicalAnalysis::Rsi, + TechnicalAnalysis::Sma.indicator_symbol => TechnicalAnalysis::Sma, + TechnicalAnalysis::Sr.indicator_symbol => TechnicalAnalysis::Sr, + TechnicalAnalysis::Trix.indicator_symbol => TechnicalAnalysis::Trix, + TechnicalAnalysis::Tsi.indicator_symbol => TechnicalAnalysis::Tsi, + TechnicalAnalysis::Uo.indicator_symbol => TechnicalAnalysis::Uo, + TechnicalAnalysis::Vi.indicator_symbol => TechnicalAnalysis::Vi, + TechnicalAnalysis::Vpt.indicator_symbol => TechnicalAnalysis::Vpt, + TechnicalAnalysis::Vwap.indicator_symbol => TechnicalAnalysis::Vwap, + TechnicalAnalysis::Wr.indicator_symbol => TechnicalAnalysis::Wr, + } + end + + # Finds the applicable indicator and returns an instance + # + # @param indicator_symbol [String] Downcased string of the indicator symbol + # + # @return TechnicalAnalysis::ClassName + def self.find(indicator_symbol) + if roster_hash.key?(indicator_symbol) + roster_hash[indicator_symbol] + else + nil + end + end + + # Find the applicable indicator and looks up the value + # + # @param indicator_symbol [String] Downcased string of the indicator symbol + # @param data [Array] Array of hashes of price data to perform calcualtion on + # @param calculation [Symbol] The calculation to be performed on the requested indicator and params + # @param options [Hash] A hash containing options for the requested calculation + # + # @return Returns the requested calculation + def self.calculate(indicator_symbol, data, calculation, options={}) + return nil unless CALCULATIONS.include? calculation + + indicator = find(indicator_symbol) + raise "Indicator not found!" if indicator.nil? + + case calculation + when :indicator_name; indicator.indicator_name + when :indicator_symbol; indicator.indicator_symbol + when :technicals; indicator.calculate(data, options) + when :min_data_size; indicator.min_data_size(options) + when :valid_options; indicator.valid_options + when :validate_options; indicator.validate_options(options) + else nil + end + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(indicator_symbol, options) + raise "#{self.name} did not implement min_data_size" + nil + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + raise "#{self.name} did not implement validate_options" + false + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + raise "#{self.name} did not implement valid_options" + [] + end + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + raise "#{self.name} did not implement indicator_symbol" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + raise "#{self.name} did not implement indicator_name" + end + + end +end diff --git a/lib/technical_analysis/indicators/kc.rb b/lib/technical_analysis/indicators/kc.rb new file mode 100644 index 0000000..6ce3cd6 --- /dev/null +++ b/lib/technical_analysis/indicators/kc.rb @@ -0,0 +1,124 @@ +module TechnicalAnalysis + # Keltner Channel + class Kc < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "kc" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Keltner Channel" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 10) + period.to_i + end + + # Calculates the keltner channel (KC) for the data over the given period + # https://en.wikipedia.org/wiki/Keltner_channel + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low, :close) + # @param period [Integer] The given period to calculate the KC + # + # @return [Array] An array of KcValue instances + def self.calculate(data, period: 10) + period = period.to_i + Validation.validate_numeric_data(data, :high, :low, :close) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + period_values = [] + + data.each do |v| + tp = StockCalculation.typical_price(v) + tr = v[:high] - v[:low] + period_values << { typical_price: tp, trading_range: tr } + + if period_values.size == period + mb = ArrayHelper.average(period_values.map { |pv| pv[:typical_price] }) + + trading_range_average = ArrayHelper.average(period_values.map { |pv| pv[:trading_range] }) + ub = mb + trading_range_average + lb = mb - trading_range_average + + output << KcValue.new( + date_time: v[:date_time], + lower_band: lb, + middle_band: mb, + upper_band: ub + ) + + period_values.shift + end + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class KcValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the lower_band calculation value + attr_accessor :lower_band + + # @return [Float] the middle_band calculation value + attr_accessor :middle_band + + # @return [Float] the upper_band calculation value + attr_accessor :upper_band + + def initialize(date_time: nil, lower_band: nil, middle_band: nil, upper_band: nil) + @date_time = date_time + @lower_band = lower_band + @middle_band = middle_band + @upper_band = upper_band + end + + # @return [Hash] the attributes as a hash + def to_hash + { + date_time: @date_time, + lower_band: @lower_band, + middle_band: @middle_band, + upper_band: @upper_band + } + end + + end +end diff --git a/lib/technical_analysis/indicators/kst.rb b/lib/technical_analysis/indicators/kst.rb new file mode 100644 index 0000000..076deee --- /dev/null +++ b/lib/technical_analysis/indicators/kst.rb @@ -0,0 +1,132 @@ +module TechnicalAnalysis + # Know Sure Thing + class Kst < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "kst" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Know Sure Thing" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period roc1 roc2 roc3 roc4 sma1 sma2 sma3 sma4 price_key) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(roc4: 30, sma4: 15, **params) + roc4.to_i + sma4.to_i - 1 + end + + # Calculates the know sure thing (KST) for the data over the given period + # https://en.wikipedia.org/wiki/KST_oscillator + # + # @param data [Array] Array of hashes with keys (:date_time, :value) + # @param roc1 [Integer] The given period to calculate the rate-of-change for RCMA1 + # @param roc2 [Integer] The given period to calculate the rate-of-change for RCMA2 + # @param roc3 [Integer] The given period to calculate the rate-of-change for RCMA3 + # @param roc4 [Integer] The given period to calculate the rate-of-change for RCMA4 + # @param sma1 [Integer] The given period to calculate the SMA of the rate-of-change for RCMA1 + # @param sma2 [Integer] The given period to calculate the SMA of the rate-of-change for RCMA2 + # @param sma3 [Integer] The given period to calculate the SMA of the rate-of-change for RCMA3 + # @param sma4 [Integer] The given period to calculate the SMA of the rate-of-change for RCMA4 + # @param price_key [Symbol] The hash key for the price data. Default :value + # + # @return [Array] An array of KstValue instances + def self.calculate(data, roc1: 10, roc2: 15, roc3: 20, roc4: 30, sma1: 10, sma2: 10, sma3: 10, sma4: 15, price_key: :value) + roc1 = roc1.to_i + roc2 = roc2.to_i + roc3 = roc3.to_i + roc4 = roc4.to_i + sma1 = sma1.to_i + sma2 = sma2.to_i + sma3 = sma3.to_i + sma4 = sma4.to_i + price_key = price_key.to_sym + Validation.validate_numeric_data(data, price_key) + Validation.validate_length(data, min_data_size(roc4: roc4, sma4: sma4)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + index = roc4 + sma4 - 2 + output = [] + + while index < data.size + date_time = data[index][:date_time] + rcma1 = calculate_rcma(data, index, price_key, roc1, sma1) + rcma2 = calculate_rcma(data, index, price_key, roc2, sma2) + rcma3 = calculate_rcma(data, index, price_key, roc3, sma3) + rcma4 = calculate_rcma(data, index, price_key, roc4, sma4) + + kst = (1 * rcma1) + (2 * rcma2) + (3 * rcma3) + (4 * rcma4) + + output << KstValue.new(date_time: date_time, kst: kst) + + index += 1 + end + + output.sort_by(&:date_time).reverse + end + + private_class_method def self.calculate_rcma(data, index, price_key, roc, sma) + roc_data = [] + index_range = (index - sma + 1)..index + + index_range.each do |i| + last_price = data[i][price_key] + starting_price = data[i - roc + 1][price_key] + + roc_data << (last_price - starting_price) / starting_price * 100 + end + + ArrayHelper.sum(roc_data) / sma.to_f + end + + end + + # The value class to be returned by calculations + class KstValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the kst calculation value + attr_accessor :kst + + def initialize(date_time: nil, kst: nil) + @date_time = date_time + @kst = kst + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, kst: @kst } + end + + end +end diff --git a/lib/technical_analysis/indicators/macd.rb b/lib/technical_analysis/indicators/macd.rb new file mode 100644 index 0000000..a1c988e --- /dev/null +++ b/lib/technical_analysis/indicators/macd.rb @@ -0,0 +1,144 @@ +module TechnicalAnalysis + # Moving Average Convergence Divergence + class Macd < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "macd" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Moving Average Convergence Divergence" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(fast_period slow_period signal_period price_key) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(slow_period: 26, signal_period: 9, **params) + slow_period.to_i + signal_period.to_i - 1 + end + + # Calculates the moving average convergence divergence (MACD) for the data over the given period + # https://en.wikipedia.org/wiki/MACD + # + # @param data [Array] Array of hashes with keys (:date_time, :value) + # @param fast_period [Integer] The given period to calculate the fast moving EMA for MACD + # @param slow_period [Integer] The given period to calculate the slow moving EMA for MACD + # @param signal_period [Integer] The given period to calculate the signal line for MACD + # @param price_key [Symbol] The hash key for the price data. Default :value + # + # @return [Array] An array of MacdValue instances + def self.calculate(data, fast_period: 12, slow_period: 26, signal_period: 9, price_key: :value) + fast_period = fast_period.to_i + slow_period = slow_period.to_i + signal_period = signal_period.to_i + price_key = price_key.to_sym + Validation.validate_numeric_data(data, price_key) + Validation.validate_length(data, min_data_size(slow_period: slow_period, signal_period: signal_period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + macd_values = [] + output = [] + period_values = [] + prev_fast_ema = nil + prev_signal = nil + prev_slow_ema = nil + + data.each do |v| + period_values << v[price_key] + + if period_values.size >= fast_period + fast_ema = StockCalculation.ema(v[price_key], period_values, fast_period, prev_fast_ema) + prev_fast_ema = fast_ema + + if period_values.size == slow_period + slow_ema = StockCalculation.ema(v[price_key], period_values, slow_period, prev_slow_ema) + prev_slow_ema = slow_ema + + macd = fast_ema - slow_ema + macd_values << macd + + if macd_values.size == signal_period + signal = StockCalculation.ema(macd, macd_values, signal_period, prev_signal) + prev_signal = signal + + output << MacdValue.new( + date_time: v[:date_time], + macd_line: macd, + signal_line: signal, + macd_histogram: macd - signal, + ) + + macd_values.shift + end + + period_values.shift + end + end + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class MacdValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the macd_line calculation value + attr_accessor :macd_line + + # @return [Float] the macd_histogram calculation value + attr_accessor :macd_histogram + + # @return [Float] the signal_line calculation value + attr_accessor :signal_line + + def initialize(date_time: nil, macd_line: nil, macd_histogram: nil, signal_line: nil) + @date_time = date_time + @macd_line = macd_line + @macd_histogram = macd_histogram + @signal_line = signal_line + end + + # @return [Hash] the attributes as a hash + def to_hash + { + date_time: @date_time, + macd_line: @macd_line, + macd_histogram: @macd_histogram, + signal_line: @signal_line + } + end + + end +end diff --git a/lib/technical_analysis/indicators/mfi.rb b/lib/technical_analysis/indicators/mfi.rb new file mode 100644 index 0000000..942e043 --- /dev/null +++ b/lib/technical_analysis/indicators/mfi.rb @@ -0,0 +1,126 @@ +module TechnicalAnalysis + # Monoey Flow Index + class Mfi < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "mfi" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Money Flow Index" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 14) + period.to_i + 1 + end + + # Calculates the money flow index (MFI) for the data over the given period + # https://en.wikipedia.org/wiki/Money_flow_index + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low, :close, :volume) + # @param period [Integer] The given period to calculate the MFI + # + # @return [Array] An array of MfiValue instances + def self.calculate(data, period: 14) + period = period.to_i + Validation.validate_numeric_data(data, :high, :low, :close, :volume) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + prev_typical_price = StockCalculation.typical_price(data.first) + raw_money_flows = [] + + data.shift + + data.each do |v| + typical_price = StockCalculation.typical_price(v) + + if typical_price < prev_typical_price + money_flow = (-1.0 * typical_price * v[:volume]) + elsif typical_price > prev_typical_price + money_flow = (typical_price * v[:volume]) + else + money_flow = 0.0 + end + + raw_money_flows << money_flow + + if raw_money_flows.size == period + positive_period_flows = ArrayHelper.sum(raw_money_flows.map { |rmf| rmf.positive? ? rmf : 0 }) + negative_period_flows = ArrayHelper.sum(raw_money_flows.map { |rmf| rmf.negative? ? rmf.abs : 0 }) + + if positive_period_flows == 0 + money_flow_ratio = 0 + elsif negative_period_flows == 0 + money_flow_ratio = positive_period_flows + else + money_flow_ratio = (positive_period_flows / negative_period_flows) + end + + mfi = (100.00 - (100.00 / (1.0 + money_flow_ratio))) + + output << MfiValue.new(date_time: v[:date_time], mfi: mfi) + + raw_money_flows.shift + end + + prev_typical_price = typical_price + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class MfiValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the mfi calculation value + attr_accessor :mfi + + def initialize(date_time: nil, mfi: nil) + @date_time = date_time + @mfi = mfi + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, mfi: @mfi } + end + + end +end diff --git a/lib/technical_analysis/indicators/mi.rb b/lib/technical_analysis/indicators/mi.rb new file mode 100644 index 0000000..8fb495c --- /dev/null +++ b/lib/technical_analysis/indicators/mi.rb @@ -0,0 +1,121 @@ +module TechnicalAnalysis + # Mass Index + class Mi < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "mi" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Mass Index" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(ema_period sum_period) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(ema_period: 9, sum_period: 25) + (ema_period.to_i * 2) + sum_period.to_i - 2 + end + + # Calculates the mass index (MI) for the data over the given period + # https://en.wikipedia.org/wiki/Mass_index + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low) + # @param ema_period [Integer] The given period to calculate the EMA and EMA of EMA + # @param sum_period [Integer] The given period to calculate the sum of EMA ratios + # + # @return [Array] An array of MiValue instances + def self.calculate(data, ema_period: 9, sum_period: 25) + ema_period = ema_period.to_i + sum_period = sum_period.to_i + Validation.validate_numeric_data(data, :high, :low) + Validation.validate_length(data, min_data_size(ema_period: ema_period, sum_period: sum_period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + double_emas = [] + high_low_diffs = [] + output = [] + ratio_of_emas = [] + single_emas = [] + + data.each do |v| + high_low_diff = v[:high] - v[:low] + high_low_diffs << high_low_diff + + if high_low_diffs.size == ema_period + single_ema = StockCalculation.ema(high_low_diff, high_low_diffs, ema_period, single_emas.last) + single_emas << single_ema + + if single_emas.size == ema_period + double_ema = StockCalculation.ema(single_emas.last, single_emas, ema_period, double_emas.last) + double_emas << double_ema + + ratio_of_emas << (single_ema / double_ema) + + if ratio_of_emas.size == sum_period + output << MiValue.new(date_time: v[:date_time], mi: ArrayHelper.sum(ratio_of_emas)) + + double_emas.shift + ratio_of_emas.shift + end + + single_emas.shift + end + + high_low_diffs.shift + end + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class MiValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the mi calculation value + attr_accessor :mi + + def initialize(date_time: nil, mi: nil) + @date_time = date_time + @mi = mi + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, mi: @mi } + end + + end +end diff --git a/lib/technical_analysis/indicators/nvi.rb b/lib/technical_analysis/indicators/nvi.rb new file mode 100644 index 0000000..5936278 --- /dev/null +++ b/lib/technical_analysis/indicators/nvi.rb @@ -0,0 +1,102 @@ +module TechnicalAnalysis + # Negative Volume Index + class Nvi < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "nvi" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Negative Volume Index" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + [] + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + return true if options == {} + raise Validation::ValidationError.new "This indicator doesn't accept any options." + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(**params) + 1 + end + + # Calculates the negative volume index (NVI) for the data + # https://en.wikipedia.org/wiki/Negative_volume_index + # + # @param data [Array] Array of hashes with keys (:date_time, :close, :volume) + # + # @return [Array] An array of NviValue instances + def self.calculate(data) + Validation.validate_numeric_data(data, :close, :volume) + Validation.validate_length(data, min_data_size({})) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + nvi_cumulative = 1_000.00 + output = [] + prev_price = data.shift + + output << NviValue.new(date_time: prev_price[:date_time], nvi: nvi_cumulative) # Start with default of 1_000 + + data.each do |v| + volume_change = ((v[:volume] - prev_price[:volume]) / prev_price[:volume]) + + if volume_change < 0 + price_change = ((v[:close] - prev_price[:close]) / prev_price[:close]) * 100.00 + nvi_cumulative += price_change + end + + output << NviValue.new(date_time: v[:date_time], nvi: nvi_cumulative) + prev_price = v + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class NviValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the nvi calculation value + attr_accessor :nvi + + def initialize(date_time: nil, nvi: nil) + @date_time = date_time + @nvi = nvi + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, nvi: @nvi } + end + + end +end diff --git a/lib/technical_analysis/indicators/obv.rb b/lib/technical_analysis/indicators/obv.rb new file mode 100644 index 0000000..67e6cd8 --- /dev/null +++ b/lib/technical_analysis/indicators/obv.rb @@ -0,0 +1,104 @@ +module TechnicalAnalysis + # On-balance Volume + class Obv < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "obv" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "On-balance Volume" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + [] + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + return true if options == {} + raise Validation::ValidationError.new "This indicator doesn't accept any options." + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(**params) + 1 + end + + # Calculates the on-balance volume (OBV) for the data over the given period + # https://en.wikipedia.org/wiki/On-balance_volume + # + # @param data [Array] Array of hashes with keys (:date_time, :close, :volume) + # + # @return [Array] An array of ObvValue instances + def self.calculate(data) + Validation.validate_numeric_data(data, :close, :volume) + Validation.validate_length(data, min_data_size({})) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + current_obv = 0 + output = [] + prior_close = nil + prior_volume = nil + + data.each do |v| + volume = v[:volume] + close = v[:close] + + unless prior_close.nil? + current_obv += volume if close > prior_close + current_obv -= volume if close < prior_close + end + + output << ObvValue.new(date_time: v[:date_time], obv: current_obv) + + prior_volume = volume + prior_close = close + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class ObvValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the obv calculation value + attr_accessor :obv + + def initialize(date_time: nil, obv: nil) + @date_time = date_time + @obv = obv + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, obv: @obv } + end + + end +end diff --git a/lib/technical_analysis/indicators/obv_mean.rb b/lib/technical_analysis/indicators/obv_mean.rb new file mode 100644 index 0000000..6f70fff --- /dev/null +++ b/lib/technical_analysis/indicators/obv_mean.rb @@ -0,0 +1,110 @@ +module TechnicalAnalysis + # On-balance Volume Mean + class ObvMean < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "obv_mean" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "On-balance Volume Mean" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 10) + period.to_i + 1 + end + + # Calculates the on-balance volume mean (OBV mean) for the data over the given period + # https://en.wikipedia.org/wiki/On-balance_volume + # + # @param data [Array] Array of hashes with keys (:date_time, :close, :volume) + # @param period [Integer] The given period to calculate the OBV mean + # + # @return [Array] An array of ObvMeanValue instances + def self.calculate(data, period: 10) + period = period.to_i + Validation.validate_numeric_data(data, :close, :volume) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + current_obv = 0 + obvs = [] + output = [] + prior_close = nil + prior_volume = nil + + data.each do |v| + volume = v[:volume] + close = v[:close] + + unless prior_close.nil? + current_obv += volume if close > prior_close + current_obv -= volume if close < prior_close + obvs << current_obv + end + + prior_volume = volume + prior_close = close + + if obvs.size == period + output << ObvMeanValue.new(date_time: v[:date_time], obv_mean: ArrayHelper.average(obvs)) + obvs.shift + end + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class ObvMeanValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the obv_mean calculation value + attr_accessor :obv_mean + + def initialize(date_time: nil, obv_mean: nil) + @date_time = date_time + @obv_mean = obv_mean + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, obv_mean: @obv_mean } + end + + end +end diff --git a/lib/technical_analysis/indicators/rsi.rb b/lib/technical_analysis/indicators/rsi.rb new file mode 100644 index 0000000..ca5cbd5 --- /dev/null +++ b/lib/technical_analysis/indicators/rsi.rb @@ -0,0 +1,134 @@ +module TechnicalAnalysis + # Relative Strength Index + class Rsi < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "rsi" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Relative Strength Index" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period price_key date_time_key) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 14, **params) + period.to_i + 1 + end + + # Calculates the relative strength index for the data over the given period + # https://en.wikipedia.org/wiki/Relative_strength_index + # + # @param data [Array] Array of hashes with keys (:date_time, :value) + # @param period [Integer] The given period to calculate the RSI + # @param price_key [Symbol] The hash key for the price data. Default :value + # @param date_time_key [Symbol] The hash key for the date time data. Default :date_time + # + # @return [Array] An array of RsiValue instances + def self.calculate(data, period: 14, price_key: :value, date_time_key: :date_time) + period = period.to_i + price_key = price_key.to_sym + Validation.validate_numeric_data(data, price_key) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data, date_time_key) + + data = data.sort_by { |row| row[date_time_key] } + + output = [] + prev_price = data.shift[price_key] + prev_avg = nil + price_changes = [] + smoothing_period = period - 1 + + data.each do |v| + price_change = (v[price_key] - prev_price) + price_changes << price_change + + if price_changes.size == period + if prev_avg.nil? + avg_gain = ArrayHelper.average(price_changes.map { |pc| pc.positive? ? pc : 0 }) + avg_loss = ArrayHelper.average(price_changes.map { |pc| pc.negative? ? pc.abs : 0 }) + else + if price_change > 0 + current_loss = 0 + current_gain = price_change + elsif price_change < 0 + current_loss = price_change.abs + current_gain = 0 + else + current_loss = 0 + current_gain = 0 + end + + avg_gain = (((prev_avg[:gain] * smoothing_period) + current_gain) / period.to_f) + avg_loss = (((prev_avg[:loss] * smoothing_period) + current_loss) / period.to_f) + end + + if avg_loss == 0 + rsi = 100 + else + rs = avg_gain / avg_loss + rsi = (100.00 - (100.00 / (1.00 + rs))) + end + + output << RsiValue.new(date_time: v[date_time_key], rsi: rsi) + + prev_avg = { gain: avg_gain, loss: avg_loss } + price_changes.shift + end + + prev_price = v[price_key] + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class RsiValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the rsi calculation value + attr_accessor :rsi + + def initialize(date_time: nil, rsi: nil) + @date_time = date_time + @rsi = rsi + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, rsi: @rsi } + end + + end +end diff --git a/lib/technical_analysis/indicators/sma.rb b/lib/technical_analysis/indicators/sma.rb index 813ee96..b01ea3c 100644 --- a/lib/technical_analysis/indicators/sma.rb +++ b/lib/technical_analysis/indicators/sma.rb @@ -1,28 +1,100 @@ module TechnicalAnalysis - class Sma - # Calculates the simple moving average for the data over the given period + # Simple Moving Average + class Sma < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "sma" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Simple Moving Average" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period price_key date_time_key) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 30, **params) + period.to_i + end + + # Calculates the simple moving average (SMA) for the data over the given period # https://en.wikipedia.org/wiki/Moving_average#Simple_moving_average - # - # @param data [Hash] Date strings to price values + # + # @param data [Array] Array of hashes with keys (:date_time, :value) # @param period [Integer] The given period to calculate the SMA - # @return [Hash] A hash of date strings to SMA values - def self.calculate(data, period: 30) - Validation.validate_price_data(data) - Validation.validate_length(data, period) - - output = {} + # @param price_key [Symbol] The hash key for the price data. Default :value + # @param date_time_key [Symbol] The hash key for the date time data. Default :date_time + # + # @return [Array] An array of SmaValue instances + def self.calculate(data, period: 30, price_key: :value, date_time_key: :date_time) + period = period.to_i + price_key = price_key.to_sym + date_time_key = date_time_key.to_sym + Validation.validate_numeric_data(data, price_key) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data, date_time_key) + + data = data.sort_by { |row| row[date_time_key] } + + output = [] period_values = [] - data = data.sort.to_h # Sort data by descending dates - - data.each do |date, price| - period_values << price + + data.each do |v| + period_values << v[price_key] if period_values.size == period - output[date] = period_values.sum / period.to_f + output << SmaValue.new(date_time: v[date_time_key], sma: ArrayHelper.average(period_values)) period_values.shift end end - output + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class SmaValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the sma calculation value + attr_accessor :sma + + def initialize(date_time: nil, sma: nil) + @date_time = date_time + @sma = sma end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, sma: @sma } + end + end end diff --git a/lib/technical_analysis/indicators/sr.rb b/lib/technical_analysis/indicators/sr.rb new file mode 100644 index 0000000..6f64f34 --- /dev/null +++ b/lib/technical_analysis/indicators/sr.rb @@ -0,0 +1,122 @@ +module TechnicalAnalysis + # Stochastic Oscillator + class Sr < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "sr" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Stochastic Oscillator" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period signal_period) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 14, signal_period: 3) + period.to_i + signal_period.to_i - 1 + end + + # Calculates the stochastic oscillator (%K) for the data over the given period + # https://en.wikipedia.org/wiki/Stochastic_oscillator + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low, :close) + # @param period [Integer] The given period to calculate the SR + # @param signal_period [Integer] The given period to calculate the SMA as a signal line for SR + # + # @return [Array] An array of SrValue instances + def self.calculate(data, period: 14, signal_period: 3) + period = period.to_i + signal_period = signal_period.to_i + Validation.validate_numeric_data(data, :high, :low, :close) + Validation.validate_length(data, min_data_size(period: period, signal_period: signal_period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + high_low_values = [] + output = [] + sr_values = [] + + data.each do |v| + high_low_values << { high: v[:high], low: v[:low] } + + if high_low_values.size == period + lowest_low = high_low_values.map { |hlv| hlv[:low] }.min + highest_high = high_low_values.map { |hlv| hlv[:high] }.max + + sr = (v[:close] - lowest_low) / (highest_high - lowest_low) * 100.00 + + sr_values << sr + + if sr_values.size == signal_period + signal = ArrayHelper.average(sr_values) + + output << SrValue.new( + date_time: v[:date_time], + sr: sr, + sr_signal: signal + ) + + sr_values.shift + end + + high_low_values.shift + end + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class SrValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the sr calculation value + attr_accessor :sr + + # @return [Float] the sr_signal calculation value + attr_accessor :sr_signal + + def initialize(date_time: nil, sr: nil, sr_signal: nil) + @date_time = date_time + @sr = sr + @sr_signal = sr_signal + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, sr: @sr, sr_signal: @sr_signal } + end + + end +end diff --git a/lib/technical_analysis/indicators/trix.rb b/lib/technical_analysis/indicators/trix.rb new file mode 100644 index 0000000..5bd3b28 --- /dev/null +++ b/lib/technical_analysis/indicators/trix.rb @@ -0,0 +1,127 @@ +module TechnicalAnalysis + # Triple Exponential Average + class Trix < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "trix" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Triple Exponential Average" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period price_key) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 15, **params) + (period.to_i * 3) - 1 + end + + # Calculates the triple exponential average (Trix) for the data over the given period + # https://en.wikipedia.org/wiki/Trix_(technical_analysis) + # + # @param data [Array] Array of hashes with keys (:date_time, :value) + # @param period [Integer] The given period to calculate the EMA for Trix + # @param price_key [Symbol] The hash key for the price data. Default :value + # + # @return [Array] An array of TrixValue instances + def self.calculate(data, period: 15, price_key: :value) + period = period.to_i + price_key = price_key.to_sym + Validation.validate_numeric_data(data, price_key) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + ema1 = [] + ema2 = [] + ema3 = [] + output = [] + period_values = [] + + data.each do |v| + price = v[price_key] + period_values << price + + if period_values.size == period + ema1_value = StockCalculation.ema(price, period_values, period, ema1.last) + ema1 << ema1_value + + if ema1.size == period + ema2_value = StockCalculation.ema(ema1_value, ema1, period, ema2.last) + ema2 << ema2_value + + if ema2.size == period + ema3_value = StockCalculation.ema(ema2_value, ema2, period, ema3.last) + ema3 << ema3_value + + if ema3.size == 2 + prev_ema3, current_ema3 = ema3 + trix = ((current_ema3 - prev_ema3) / prev_ema3) + output << TrixValue.new(date_time: v[:date_time], trix: trix) + + ema3.shift + end + + ema2.shift + end + + ema1.shift + end + + period_values.shift + end + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class TrixValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the trix calculation value + attr_accessor :trix + + def initialize(date_time: nil, trix: nil) + @date_time = date_time + @trix = trix + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, trix: @trix } + end + + end +end diff --git a/lib/technical_analysis/indicators/tsi.rb b/lib/technical_analysis/indicators/tsi.rb new file mode 100644 index 0000000..ad9474b --- /dev/null +++ b/lib/technical_analysis/indicators/tsi.rb @@ -0,0 +1,139 @@ +module TechnicalAnalysis + # True Strength Index + class Tsi < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "tsi" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "True Strength Index" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(low_period high_period price_key) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(low_period: 13, high_period: 25, **params) + low_period.to_i + high_period.to_i + end + + # Calculates the true strength index (TSI) for the data over the given period + # https://en.wikipedia.org/wiki/True_strength_index + # + # @param data [Array] Array of hashes with keys (:date_time, :value) + # @param high_period [Integer] The given high period to calculate the EMA + # @param low_period [Integer] The given low period to calculate the EMA + # @param price_key [Symbol] The hash key for the price data. Default :value + # + # @return [Array] An array of TsiValue instances + def self.calculate(data, low_period: 13, high_period: 25, price_key: :value) + low_period = low_period.to_i + high_period = high_period.to_i + price_key = price_key.to_sym + Validation.validate_numeric_data(data, price_key) + Validation.validate_length(data, min_data_size(low_period: low_period, high_period: high_period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + high_emas = [] + high_multiplier = (2.0 / (high_period + 1.0)) + low_emas = [] + low_multiplier = (2.0 / (low_period + 1.0)) + momentum_values = [] + output = [] + prev_price = data.shift[price_key] + + data.each do |v| + current_price = v[price_key] + momentum = current_price - prev_price + momentum_hash = { value: momentum, abs_value: momentum.abs } + + momentum_values << momentum_hash + + if momentum_values.size == high_period + high_emas << process_ema(momentum_hash, momentum_values, high_multiplier, high_period, high_emas) + + if high_emas.size == low_period + low_ema = process_ema(high_emas.last, high_emas, low_multiplier, low_period, low_emas) + low_emas << low_ema + + output << TsiValue.new( + date_time: v[:date_time], + tsi: ((100 * (low_ema[:value] / low_ema[:abs_value]))) + ) + + low_emas.shift if low_emas.size > 1 # Only need to retain the last low_ema + high_emas.shift + end + + momentum_values.shift + end + + prev_price = current_price + end + + output.sort_by(&:date_time).reverse + end + + private_class_method def self.process_ema(current_value, data, multiplier, period, store) + if store.empty? + value = ArrayHelper.average(data.map { |d| d[:value] }) + abs_value = ArrayHelper.average(data.map { |d| d[:abs_value] }) + else + prev_value = store.last + value = ((multiplier * (current_value[:value] - prev_value[:value])) + prev_value[:value]) + abs_value = ((multiplier * (current_value[:abs_value] - prev_value[:abs_value])) + prev_value[:abs_value]) + end + + { value: value, abs_value: abs_value } + end + + end + + # The value class to be returned by calculations + class TsiValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the tsi calculation value + attr_accessor :tsi + + def initialize(date_time: nil, tsi: nil) + @date_time = date_time + @tsi = tsi + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, tsi: @tsi } + end + + end +end diff --git a/lib/technical_analysis/indicators/uo.rb b/lib/technical_analysis/indicators/uo.rb new file mode 100644 index 0000000..3a53977 --- /dev/null +++ b/lib/technical_analysis/indicators/uo.rb @@ -0,0 +1,130 @@ +module TechnicalAnalysis + # Ultimate Oscillator + class Uo < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "uo" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Ultimate Oscillator" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(short_period medium_period long_period short_weight medium_weight long_weight) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(long_period: 28, **params) + long_period.to_i + 1 + end + + # Calculates the ultimate oscillator (UO) for the data over the given period + # https://en.wikipedia.org/wiki/Ultimate_oscillator + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low, :close) + # @param short_period [Integer] The given short period + # @param medium_period [Integer] The given medium period + # @param long_period [Integer] The given long period + # @param short_weight [Float] Weight of short Buying Pressure average for UO + # @param medium_weight [Float] Weight of medium Buying Pressure average for UO + # @param long_weight [Float] Weight of long Buying Pressure average for UO + # + # @return [Array] An array of UoValue instances + def self.calculate(data, short_period: 7, medium_period: 14, long_period: 28, short_weight: 4, medium_weight: 2, long_weight: 1) + short_period = short_period.to_i + medium_period = medium_period.to_i + long_period = long_period.to_i + short_weight = short_weight.to_f + medium_weight = medium_weight.to_f + long_weight = long_weight.to_f + Validation.validate_numeric_data(data, :high, :low, :close) + Validation.validate_length(data, min_data_size(long_period: long_period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + period_values = [] + prior_close = data.shift[:close] + sum_of_weights = ArrayHelper.sum([short_weight, medium_weight, long_weight]) + + data.each do |v| + min_low_p_close = [v[:low], prior_close].min + max_high_p_close = [v[:high], prior_close].max + + buying_pressure = v[:close] - min_low_p_close + true_range = max_high_p_close - min_low_p_close + + period_values << { buying_pressure: buying_pressure, true_range: true_range } + + if period_values.size == long_period + short_average = calculate_average(short_period, period_values) + medium_average = calculate_average(medium_period, period_values) + long_average = calculate_average(long_period, period_values) + uo = 100 * (((short_weight * short_average) + (medium_weight * medium_average) + (long_weight * long_average)) / (sum_of_weights)) + + output << UoValue.new(date_time: v[:date_time], uo: uo) + + period_values.shift + end + + prior_close = v[:close] + end + + output.sort_by(&:date_time).reverse + end + + private_class_method def self.calculate_average(period, data) + buying_pressures_sum = ArrayHelper.sum(data.last(period).map { |d| d[:buying_pressure] }) + true_ranges_sum = ArrayHelper.sum(data.last(period).map { |d| d[:true_range] }) + + buying_pressures_sum / true_ranges_sum + end + + end + + # The value class to be returned by calculations + class UoValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the uo calculation value + attr_accessor :uo + + def initialize(date_time: nil, uo: nil) + @date_time = date_time + @uo = uo + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, uo: @uo } + end + + end +end diff --git a/lib/technical_analysis/indicators/vi.rb b/lib/technical_analysis/indicators/vi.rb new file mode 100644 index 0000000..2a7f9e1 --- /dev/null +++ b/lib/technical_analysis/indicators/vi.rb @@ -0,0 +1,117 @@ +module TechnicalAnalysis + # Vortex Indicator + class Vi < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "vi" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Vortex Indicator" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 14) + period.to_i + 1 + end + + # Calculates the vortex indicator (VI) for the data over the given period + # https://en.wikipedia.org/wiki/Vortex_indicator + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low, :close) + # @param period [Integer] The given period to calculate the VI + # + # @return [Array] An array of ViValue instances + def self.calculate(data, period: 14) + period = period.to_i + Validation.validate_numeric_data(data, :high, :low, :close) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + period_values = [] + prev_price = data.shift + + data.each do |v| + positive_vm = (v[:high] - prev_price[:low]).abs + negative_vm = (v[:low] - prev_price[:high]).abs + tr = [(v[:high] - v[:low]), (v[:high] - prev_price[:close]).abs, (v[:low] - prev_price[:close]).abs].max + + period_values << { pos_vm: positive_vm, neg_vm: negative_vm, tr: tr } + + if period_values.size == period + pos_vm_period = ArrayHelper.sum(period_values.map { |pv| pv[:pos_vm] }) + neg_vm_period = ArrayHelper.sum(period_values.map { |pv| pv[:neg_vm] }) + tr_period = ArrayHelper.sum(period_values.map { |pv| pv[:tr] }) + + output << ViValue.new( + date_time: v[:date_time], + positive_vi: (pos_vm_period / tr_period), + negative_vi: (neg_vm_period / tr_period), + ) + + period_values.shift + end + + prev_price = v + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class ViValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the positive Vortex Indicator value + attr_accessor :positive_vi + + # @return [Float] the negative Vortex Indicator value + attr_accessor :negative_vi + + def initialize(date_time: nil, positive_vi: nil, negative_vi: nil) + @date_time = date_time + @positive_vi = positive_vi + @negative_vi = negative_vi + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, positive_vi: @positive_vi, negative_vi: @negative_vi } + end + + end +end diff --git a/lib/technical_analysis/indicators/vpt.rb b/lib/technical_analysis/indicators/vpt.rb new file mode 100644 index 0000000..383e3de --- /dev/null +++ b/lib/technical_analysis/indicators/vpt.rb @@ -0,0 +1,95 @@ +module TechnicalAnalysis + # Volume-price Trend + class Vpt < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "vpt" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Volume-price Trend" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + [] + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + return true if options == {} + raise Validation::ValidationError.new "This indicator doesn't accept any options." + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(**params) + 2 + end + + # Calculates the volume-price trend (VPT) for the data + # https://en.wikipedia.org/wiki/Volume%E2%80%93price_trend + # + # @param data [Array] Array of hashes with keys (:date_time, :close, :volume) + # + # @return [Array] An array of VptValue instances + def self.calculate(data) + Validation.validate_numeric_data(data, :close, :volume) + Validation.validate_length(data, min_data_size({})) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + prev_price = data.shift + prev_pvt = 0 + + data.each do |v| + pvt = prev_pvt + (((v[:close] - prev_price[:close]) / prev_price[:close]) * v[:volume]) + output << VptValue.new(date_time: v[:date_time], vpt: pvt) + prev_price = v + prev_pvt = pvt + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class VptValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the vpt calculation value + attr_accessor :vpt + + def initialize(date_time: nil, vpt: nil) + @date_time = date_time + @vpt = vpt + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, vpt: @vpt } + end + + end +end diff --git a/lib/technical_analysis/indicators/vwap.rb b/lib/technical_analysis/indicators/vwap.rb new file mode 100644 index 0000000..24c3f5b --- /dev/null +++ b/lib/technical_analysis/indicators/vwap.rb @@ -0,0 +1,97 @@ +module TechnicalAnalysis + # Volume Weighted Average Price + class Vwap < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "vwap" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Volume Weighted Average Price" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + [] + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + return true if options == {} + raise Validation::ValidationError.new "This indicator doesn't accept any options." + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(**params) + 1 + end + + # Calculates the volume weighted average price (VWAP) for the data + # https://en.wikipedia.org/wiki/Volume-weighted_average_price + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low, :close, :volume) + # + # @return [Array] An array of VwapValue instances + def self.calculate(data) + Validation.validate_numeric_data(data, :high, :low, :close, :volume) + Validation.validate_length(data, min_data_size) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + cumm_volume = 0 + cumm_volume_x_typical_price = 0 + + data.each do |v| + typical_price = StockCalculation.typical_price(v) + cumm_volume_x_typical_price += v[:volume] * typical_price + cumm_volume += v[:volume] + vwap = cumm_volume_x_typical_price.to_f / cumm_volume.to_f + + output << VwapValue.new(date_time: v[:date_time], vwap: vwap) + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class VwapValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the vwap calculation value + attr_accessor :vwap + + def initialize(date_time: nil, vwap: nil) + @date_time = date_time + @vwap = vwap + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, vwap: @vwap } + end + + end +end diff --git a/lib/technical_analysis/indicators/wma.rb b/lib/technical_analysis/indicators/wma.rb new file mode 100644 index 0000000..fa6a5c9 --- /dev/null +++ b/lib/technical_analysis/indicators/wma.rb @@ -0,0 +1,100 @@ +module TechnicalAnalysis + # Weighted Moving Average + class Wma < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "wma" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Weighted Moving Average" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period price_key date_time_key) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 30, **params) + period.to_i + end + + # Calculates the weighted moving average (WMA) for the data over the given period + # https://en.wikipedia.org/wiki/Moving_average#Weighted_moving_average + # + # @param data [Array] Array of hashes with keys (:date_time, :value) + # @param period [Integer] The given period to calculate the WMA + # @param price_key [Symbol] The hash key for the price data. Default :value + # @param date_time_key [Symbol] The hash key for the date time data. Default :date_time + # + # @return [Array] An array of WmaValue instances + def self.calculate(data, period: 30, price_key: :value, date_time_key: :date_time) + period = period.to_i + price_key = price_key.to_sym + date_time_key = date_time_key.to_sym + Validation.validate_numeric_data(data, price_key) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data, date_time_key) + + data = data.sort_by { |row| row[date_time_key] } + + output = [] + period_values = [] + previous_wma = nil + + data.each do |v| + period_values << v[price_key] + if period_values.size == period + wma = StockCalculation.wma(period_values) + output << WmaValue.new(date_time: v[date_time_key], wma: wma) + period_values.shift + end + end + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class WmaValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the wma calculation value + attr_accessor :wma + + def initialize(date_time: nil, wma: nil) + @date_time = date_time + @wma = wma + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, wma: @wma } + end + end +end diff --git a/lib/technical_analysis/indicators/wr.rb b/lib/technical_analysis/indicators/wr.rb new file mode 100644 index 0000000..943a1fd --- /dev/null +++ b/lib/technical_analysis/indicators/wr.rb @@ -0,0 +1,103 @@ +module TechnicalAnalysis + # Williams %R + class Wr < Indicator + + # Returns the symbol of the technical indicator + # + # @return [String] A string of the symbol of the technical indicator + def self.indicator_symbol + "wr" + end + + # Returns the name of the technical indicator + # + # @return [String] A string of the name of the technical indicator + def self.indicator_name + "Williams %R" + end + + # Returns an array of valid keys for options for this technical indicator + # + # @return [Array] An array of keys as symbols for valid options for this technical indicator + def self.valid_options + %i(period) + end + + # Validates the provided options for this technical indicator + # + # @param options [Hash] The options for the technical indicator to be validated + # + # @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not + def self.validate_options(options) + Validation.validate_options(options, valid_options) + end + + # Calculates the minimum number of observations needed to calculate the technical indicator + # + # @param options [Hash] The options for the technical indicator + # + # @return [Integer] Returns the minimum number of observations needed to calculate the technical + # indicator based on the options provided + def self.min_data_size(period: 14) + period.to_i + end + + # Calculates the Williams %R (WR) for the data over the given period + # https://en.wikipedia.org/wiki/Williams_%25R + # + # @param data [Array] Array of hashes with keys (:date_time, :high, :low, :close) + # @param period [Integer] The given look-back period to calculate the WR + # + # @return [Array] An array of WrValue instances + def self.calculate(data, period: 14) + period = period.to_i + Validation.validate_numeric_data(data, :high, :low, :close) + Validation.validate_length(data, min_data_size(period: period)) + Validation.validate_date_time_key(data) + + data = data.sort_by { |row| row[:date_time] } + + output = [] + period_values = [] + + data.each do |v| + period_values << { high: v[:high], low: v[:low] } + + if period_values.size == period + lowest_low = period_values.map { |pv| pv[:low] }.min + highest_high = period_values.map { |pv| pv[:high] }.max + + wr = (highest_high - v[:close]) / (highest_high - lowest_low) * -100 + + output << WrValue.new(date_time: v[:date_time], wr: wr) + + period_values.shift + end + end + + output.sort_by(&:date_time).reverse + end + + end + + # The value class to be returned by calculations + class WrValue + + # @return [String] the date_time of the obversation as it was provided + attr_accessor :date_time + + # @return [Float] the wr calculation value + attr_accessor :wr + + def initialize(date_time: nil, wr: nil) + @date_time = date_time + @wr = wr + end + + # @return [Hash] the attributes as a hash + def to_hash + { date_time: @date_time, wr: @wr } + end + + end +end diff --git a/spec/helpers/array_helper_spec.rb b/spec/helpers/array_helper_spec.rb new file mode 100644 index 0000000..cbad4bd --- /dev/null +++ b/spec/helpers/array_helper_spec.rb @@ -0,0 +1,31 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'ArrayHelper' do + array_of_numbers = [1, 3, 5, 7, 9].freeze + + it 'Calculates sum' do + sum = TechnicalAnalysis::ArrayHelper.sum(array_of_numbers) + expect(sum).to eq(25) + end + + it 'Calculates mean' do + mean = TechnicalAnalysis::ArrayHelper.mean(array_of_numbers) + expect(mean).to eq(5) + end + + it 'Calculates average' do + average = TechnicalAnalysis::ArrayHelper.average(array_of_numbers) + expect(average).to eq(5) + end + + it 'Calculates sample_variance' do + sample_variance = TechnicalAnalysis::ArrayHelper.sample_variance(array_of_numbers) + expect(sample_variance).to eq(10.0) + end + + it 'Calculates standard_deviation' do + standard_deviation = TechnicalAnalysis::ArrayHelper.standard_deviation(array_of_numbers) + expect(standard_deviation).to eq(3.1622776601683795) + end +end diff --git a/spec/helpers/validaton_spec.rb b/spec/helpers/validaton_spec.rb new file mode 100644 index 0000000..8a957f9 --- /dev/null +++ b/spec/helpers/validaton_spec.rb @@ -0,0 +1,22 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Validation' do + describe "date_time validation" do + input_data = SpecHelper.get_test_data(:close) + + it 'Throws exception for invalid timestamp key' do + bad_date_time_key_data = input_data.each { |row| row[:bad_timestamp_key] = row.delete :date_time } + expect { TechnicalAnalysis::Validation.validate_date_time_key(bad_date_time_key_data) }.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + end + + describe "numeric validation" do + input_data = SpecHelper.get_test_data(:close) + + it 'Throws exception for non numeric price data' do + non_numeric_data = input_data.each { |row| row[:close] = row.delete(:close).to_s } + expect { TechnicalAnalysis::Validation.validate_numeric_data(non_numeric_data, :close) }.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..0cda646 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,26 @@ +require 'csv' + +class SpecHelper + + TEST_DATA_PATH = File.join(File.dirname(__FILE__),'ta_test_data.csv') + FLOAT_KEYS = [:open, :high, :low, :close].freeze + INTEGER_KEYS = [:volume].freeze + + def self.get_test_data(*columns, date_time_key: :date_time) + @data = CSV.read(TEST_DATA_PATH, headers: true) + columns = columns.map(&:to_sym) + output = [] + @data.each do |v| + col_hash = { date_time_key => v["date_time"] } + columns.each do |col| + value = v[col.to_s] + value = value.to_f if FLOAT_KEYS.include?(col) + value = value.to_i if INTEGER_KEYS.include?(col) + col_hash[col] = value + end + output << col_hash + end + output + end + +end diff --git a/spec/ta_test_data.csv b/spec/ta_test_data.csv new file mode 100644 index 0000000..8524dca --- /dev/null +++ b/spec/ta_test_data.csv @@ -0,0 +1,64 @@ +date_time,close,volume,open,high,low +"2019-01-09T00:00:00.000Z",153.3100,45034370.0000,151.2900,154.5300,149.6300 +"2019-01-08T00:00:00.000Z",150.7500,40622910.0000,149.5600,151.8200,148.5200 +"2019-01-07T00:00:00.000Z",147.9300,54571440.0000,148.7000,148.8300,145.9000 +"2019-01-04T00:00:00.000Z",148.2600,57423650.0000,144.5300,148.5499,143.8000 +"2019-01-03T00:00:00.000Z",142.1900,91106840.0000,143.9800,145.7200,142.0000 +"2019-01-02T00:00:00.000Z",157.9200,35637070.0000,154.8900,158.8500,154.2300 +"2018-12-31T00:00:00.000Z",157.7400,34499390.0000,158.5300,159.3600,156.4800 +"2018-12-28T00:00:00.000Z",156.2300,41740600.0000,157.5000,158.5200,154.5500 +"2018-12-27T00:00:00.000Z",156.1500,51608850.0000,155.8400,156.7700,150.0700 +"2018-12-26T00:00:00.000Z",157.1700,58133850.0000,148.3000,157.2300,146.7200 +"2018-12-24T00:00:00.000Z",146.8300,37169230.0000,148.1500,151.5500,146.5900 +"2018-12-21T00:00:00.000Z",150.7300,95497900.0000,156.8600,158.1600,149.6300 +"2018-12-20T00:00:00.000Z",156.8300,64398230.0000,160.4000,162.1100,155.3000 +"2018-12-19T00:00:00.000Z",160.8900,47597670.0000,166.0000,167.4500,159.0900 +"2018-12-18T00:00:00.000Z",166.0700,33753490.0000,165.3800,167.5300,164.3900 +"2018-12-17T00:00:00.000Z",163.9400,43250420.0000,165.4500,168.3500,162.7300 +"2018-12-14T00:00:00.000Z",165.4800,40620360.0000,169.0000,169.0800,165.2800 +"2018-12-13T00:00:00.000Z",170.9500,31754210.0000,170.4900,172.5700,169.5500 +"2018-12-12T00:00:00.000Z",169.1000,35474680.0000,170.4000,171.9200,169.0200 +"2018-12-11T00:00:00.000Z",168.6300,45968040.0000,171.6600,171.7900,167.0000 +"2018-12-10T00:00:00.000Z",169.6000,61759000.0000,165.0000,170.0900,163.3300 +"2018-12-07T00:00:00.000Z",168.4900,41678680.0000,173.4900,174.4900,168.3000 +"2018-12-06T00:00:00.000Z",174.7200,42704910.0000,171.7600,174.7800,170.4200 +"2018-12-04T00:00:00.000Z",176.6900,41141250.0000,180.9500,182.3899,176.2700 +"2018-12-03T00:00:00.000Z",184.8200,40537700.0000,184.4600,184.9400,181.2100 +"2018-11-30T00:00:00.000Z",178.5800,39424260.0000,180.2900,180.3300,177.0300 +"2018-11-29T00:00:00.000Z",179.5500,41523580.0000,182.6600,182.8000,177.7000 +"2018-11-28T00:00:00.000Z",180.9400,45941750.0000,176.7300,181.2900,174.9300 +"2018-11-27T00:00:00.000Z",174.2400,41156140.0000,171.5100,174.7700,170.8800 +"2018-11-26T00:00:00.000Z",174.6200,44662320.0000,174.2400,174.9500,170.2600 +"2018-11-23T00:00:00.000Z",172.2900,23623970.0000,174.9400,176.5950,172.1000 +"2018-11-21T00:00:00.000Z",176.7800,31096240.0000,179.7300,180.2700,176.5500 +"2018-11-20T00:00:00.000Z",176.9800,67678680.0000,178.3700,181.4700,175.5100 +"2018-11-19T00:00:00.000Z",185.8600,41626820.0000,190.0000,190.7000,184.9900 +"2018-11-16T00:00:00.000Z",193.5300,36191330.0000,190.5000,194.9695,189.4600 +"2018-11-15T00:00:00.000Z",191.4100,46271660.0000,188.3900,191.9700,186.9000 +"2018-11-14T00:00:00.000Z",186.8000,60547340.0000,193.9000,194.4800,185.9300 +"2018-11-13T00:00:00.000Z",192.2300,46725710.0000,191.6300,197.1800,191.4501 +"2018-11-12T00:00:00.000Z",194.1700,50991030.0000,199.0000,199.8500,193.7900 +"2018-11-09T00:00:00.000Z",204.4700,34317760.0000,205.5500,206.0100,202.2500 +"2018-11-08T00:00:00.000Z",208.4900,25289270.0000,209.9800,210.1200,206.7500 +"2018-11-07T00:00:00.000Z",209.9500,33291640.0000,205.9700,210.0600,204.1300 +"2018-11-06T00:00:00.000Z",203.7700,31774720.0000,201.9200,204.7200,201.6900 +"2018-11-05T00:00:00.000Z",201.5900,66072170.0000,204.3000,204.3900,198.1700 +"2018-11-02T00:00:00.000Z",207.4800,91046560.0000,209.5500,213.6500,205.4300 +"2018-11-01T00:00:00.000Z",222.2200,52954070.0000,219.0500,222.3600,216.8100 +"2018-10-31T00:00:00.000Z",218.8600,38016810.0000,216.8800,220.4500,216.6200 +"2018-10-30T00:00:00.000Z",213.3000,36487930.0000,211.1500,215.1800,209.2700 +"2018-10-29T00:00:00.000Z",212.2400,45713690.0000,219.1900,219.6900,206.0900 +"2018-10-26T00:00:00.000Z",216.3000,47191700.0000,215.9000,220.1900,212.6700 +"2018-10-25T00:00:00.000Z",219.8000,29027340.0000,217.7100,221.3800,216.7500 +"2018-10-24T00:00:00.000Z",215.0900,39992120.0000,222.6000,224.2300,214.5400 +"2018-10-23T00:00:00.000Z",222.7300,38681170.0000,215.8300,223.2500,214.7000 +"2018-10-22T00:00:00.000Z",220.6500,28751540.0000,219.7900,223.3600,218.9400 +"2018-10-19T00:00:00.000Z",219.3100,32874330.0000,218.0600,221.2600,217.4300 +"2018-10-18T00:00:00.000Z",216.0200,32389280.0000,217.8600,219.7400,213.0000 +"2018-10-17T00:00:00.000Z",221.1900,22692880.0000,222.3000,222.6400,219.3400 +"2018-10-16T00:00:00.000Z",222.1500,28802550.0000,218.9300,222.9900,216.7627 +"2018-10-15T00:00:00.000Z",217.3600,30280450.0000,221.1600,221.8300,217.2700 +"2018-10-12T00:00:00.000Z",222.1100,39494770.0000,220.4200,222.8800,216.8400 +"2018-10-11T00:00:00.000Z",214.4500,52902320.0000,214.5200,219.5000,212.3200 +"2018-10-10T00:00:00.000Z",216.3600,41084070.0000,225.4600,226.3500,216.0500 +"2018-10-09T00:00:00.000Z",226.8700,26656630.0000,223.6400,227.2700,222.2462 diff --git a/spec/technical_analysis/indicators/adi_spec.rb b/spec/technical_analysis/indicators/adi_spec.rb new file mode 100644 index 0000000..2480118 --- /dev/null +++ b/spec/technical_analysis/indicators/adi_spec.rb @@ -0,0 +1,116 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "ADI" do + indicator = TechnicalAnalysis::Adi + + describe 'Accumulation/Distribution Index' do + it 'Calculates ADI' do + + input_data = SpecHelper.get_test_data(:volume, :high, :low, :close) + output = indicator.calculate(input_data) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :adi=>-112451134.66006838}, + {:date_time=>"2019-01-08T00:00:00.000Z", :adi=>-135060226.53761944}, + {:date_time=>"2019-01-07T00:00:00.000Z", :adi=>-149339794.90125585}, + {:date_time=>"2019-01-04T00:00:00.000Z", :adi=>-170386118.17770624}, + {:date_time=>"2019-01-03T00:00:00.000Z", :adi=>-220800308.5532927}, + {:date_time=>"2019-01-02T00:00:00.000Z", :adi=>-139000081.24146464}, + {:date_time=>"2018-12-31T00:00:00.000Z", :adi=>-160289759.42328274}, + {:date_time=>"2018-12-28T00:00:00.000Z", :adi=>-155977335.67328295}, + {:date_time=>"2018-12-27T00:00:00.000Z", :adi=>-149563792.60023466}, + {:date_time=>"2018-12-26T00:00:00.000Z", :adi=>-191621153.9435182}, + {:date_time=>"2018-12-24T00:00:00.000Z", :adi=>-249091249.23371798}, + {:date_time=>"2018-12-21T00:00:00.000Z", :adi=>-215519041.4917826}, + {:date_time=>"2018-12-20T00:00:00.000Z", :adi=>-144651314.99705797}, + {:date_time=>"2018-12-19T00:00:00.000Z", :adi=>-109189734.6005822}, + {:date_time=>"2018-12-18T00:00:00.000Z", :adi=>-82088668.90680213}, + {:date_time=>"2018-12-17T00:00:00.000Z", :adi=>-84453563.11062376}, + {:date_time=>"2018-12-14T00:00:00.000Z", :adi=>-59826989.44514345}, + {:date_time=>"2018-12-13T00:00:00.000Z", :adi=>-23482456.813564237}, + {:date_time=>"2018-12-12T00:00:00.000Z", :adi=>-21169236.217537448}, + {:date_time=>"2018-12-11T00:00:00.000Z", :adi=>12348220.058324995}, + {:date_time=>"2018-12-10T00:00:00.000Z", :adi=>27031122.187761355}, + {:date_time=>"2018-12-07T00:00:00.000Z", :adi=>-25774650.00158758}, + {:date_time=>"2018-12-06T00:00:00.000Z", :adi=>13345403.439446371}, + {:date_time=>"2018-12-04T00:00:00.000Z", :adi=>-28184142.065140743}, + {:date_time=>"2018-12-03T00:00:00.000Z", :adi=>7310177.429459017}, + {:date_time=>"2018-11-30T00:00:00.000Z", :adi=>-30619198.70995107}, + {:date_time=>"2018-11-29T00:00:00.000Z", :adi=>-28229849.61904212}, + {:date_time=>"2018-11-28T00:00:00.000Z", :adi=>-16831219.815120786}, + {:date_time=>"2018-11-27T00:00:00.000Z", :adi=>-57716487.89688186}, + {:date_time=>"2018-11-26T00:00:00.000Z", :adi=>-87657844.24649628}, + {:date_time=>"2018-11-23T00:00:00.000Z", :adi=>-126035061.64521725}, + {:date_time=>"2018-11-21T00:00:00.000Z", :adi=>-104408223.70305927}, + {:date_time=>"2018-11-20T00:00:00.000Z", :adi=>-77157217.68155372}, + {:date_time=>"2018-11-19T00:00:00.000Z", :adi=>-42863658.35269459}, + {:date_time=>"2018-11-16T00:00:00.000Z", :adi=>-13921718.702957403}, + {:date_time=>"2018-11-15T00:00:00.000Z", :adi=>-31201198.431607798}, + {:date_time=>"2018-11-14T00:00:00.000Z", :adi=>-67251111.0548819}, + {:date_time=>"2018-11-13T00:00:00.000Z", :adi=>-19025685.861899547}, + {:date_time=>"2018-11-12T00:00:00.000Z", :adi=>14980297.36136794}, + {:date_time=>"2018-11-09T00:00:00.000Z", :adi=>59576412.70790268}, + {:date_time=>"2018-11-08T00:00:00.000Z", :adi=>53370009.30364728}, + {:date_time=>"2018-11-07T00:00:00.000Z", :adi=>52544543.51729703}, + {:date_time=>"2018-11-06T00:00:00.000Z", :adi=>20488006.51898352}, + {:date_time=>"2018-11-05T00:00:00.000Z", :adi=>8638028.433174696}, + {:date_time=>"2018-11-02T00:00:00.000Z", :adi=>2052056.5039138943}, + {:date_time=>"2018-11-01T00:00:00.000Z", :adi=>47686098.74235708}, + {:date_time=>"2018-10-31T00:00:00.000Z", :adi=>-2596414.5729579534}, + {:date_time=>"2018-10-30T00:00:00.000Z", :adi=>-9048353.606900878}, + {:date_time=>"2018-10-29T00:00:00.000Z", :adi=>-22322304.45292461}, + {:date_time=>"2018-10-26T00:00:00.000Z", :adi=>-17952613.497042313}, + {:date_time=>"2018-10-25T00:00:00.000Z", :adi=>-16320985.571510637}, + {:date_time=>"2018-10-24T00:00:00.000Z", :adi=>-25537009.286413625}, + {:date_time=>"2018-10-23T00:00:00.000Z", :adi=>9915241.57013943}, + {:date_time=>"2018-10-22T00:00:00.000Z", :adi=>-24060850.441556394}, + {:date_time=>"2018-10-19T00:00:00.000Z", :adi=>-17555977.138388995}, + {:date_time=>"2018-10-18T00:00:00.000Z", :adi=>-16955140.819851194}, + {:date_time=>"2018-10-17T00:00:00.000Z", :adi=>-13591269.009762233}, + {:date_time=>"2018-10-16T00:00:00.000Z", :adi=>-16341921.130974408}, + {:date_time=>"2018-10-15T00:00:00.000Z", :adi=>-37374123.7894299}, + {:date_time=>"2018-10-12T00:00:00.000Z", :adi=>-8288954.710482582}, + {:date_time=>"2018-10-11T00:00:00.000Z", :adi=>-37713866.134323865}, + {:date_time=>"2018-10-10T00:00:00.000Z", :adi=>-16199273.599504825}, + {:date_time=>"2018-10-09T00:00:00.000Z", :adi=>22411774.711174767} + ] + + expect(normalized_output).to eq(expected_output) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('adi') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Accumulation/Distribution Index') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq([]) + end + + it 'Validates options' do + valid_options = {} + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = {} + expect(indicator.min_data_size(options)).to eq(1) + end + end + end +end diff --git a/spec/technical_analysis/indicators/adtv_spec.rb b/spec/technical_analysis/indicators/adtv_spec.rb new file mode 100644 index 0000000..d9ef92e --- /dev/null +++ b/spec/technical_analysis/indicators/adtv_spec.rb @@ -0,0 +1,98 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "ADTV" do + input_data = SpecHelper.get_test_data(:volume) + indicator = TechnicalAnalysis::Adtv + + describe 'Average Daily Trading Volume' do + it 'Calculates ADTV (22 day)' do + output = indicator.calculate(input_data, period: 22, volume_key: :volume) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :adtv=>49513676.36363637}, + {:date_time=>"2019-01-08T00:00:00.000Z", :adtv=>49407791.81818182}, + {:date_time=>"2019-01-07T00:00:00.000Z", :adtv=>49431352.72727273}, + {:date_time=>"2019-01-04T00:00:00.000Z", :adtv=>48793455.45454545}, + {:date_time=>"2019-01-03T00:00:00.000Z", :adtv=>47975301.36363637}, + {:date_time=>"2019-01-02T00:00:00.000Z", :adtv=>45721516.81818182}, + {:date_time=>"2018-12-31T00:00:00.000Z", :adtv=>46189911.36363637}, + {:date_time=>"2018-12-28T00:00:00.000Z", :adtv=>46492490.90909091}, + {:date_time=>"2018-12-27T00:00:00.000Z", :adtv=>46625296.36363637}, + {:date_time=>"2018-12-26T00:00:00.000Z", :adtv=>45353256.36363637}, + {:date_time=>"2018-12-24T00:00:00.000Z", :adtv=>44124274.09090909}, + {:date_time=>"2018-12-21T00:00:00.000Z", :adtv=>45511067.27272727}, + {:date_time=>"2018-12-20T00:00:00.000Z", :adtv=>43062381.81818182}, + {:date_time=>"2018-12-19T00:00:00.000Z", :adtv=>41780250.0}, + {:date_time=>"2018-12-18T00:00:00.000Z", :adtv=>41719976.81818182}, + {:date_time=>"2018-12-17T00:00:00.000Z", :adtv=>42937879.09090909}, + {:date_time=>"2018-12-14T00:00:00.000Z", :adtv=>43095846.81818182}, + {:date_time=>"2018-12-13T00:00:00.000Z", :adtv=>43567240.90909091}, + {:date_time=>"2018-12-12T00:00:00.000Z", :adtv=>43683765.90909091}, + {:date_time=>"2018-12-11T00:00:00.000Z", :adtv=>43220792.72727273}, + {:date_time=>"2018-12-10T00:00:00.000Z", :adtv=>42644592.72727273}, + {:date_time=>"2018-12-07T00:00:00.000Z", :adtv=>41281670.90909091}, + {:date_time=>"2018-12-06T00:00:00.000Z", :adtv=>42390465.90909091}, + {:date_time=>"2018-12-04T00:00:00.000Z", :adtv=>44587813.63636363}, + {:date_time=>"2018-12-03T00:00:00.000Z", :adtv=>45124760.0}, + {:date_time=>"2018-11-30T00:00:00.000Z", :adtv=>45010174.09090909}, + {:date_time=>"2018-11-29T00:00:00.000Z", :adtv=>44876704.54545455}, + {:date_time=>"2018-11-28T00:00:00.000Z", :adtv=>45067164.09090909}, + {:date_time=>"2018-11-27T00:00:00.000Z", :adtv=>45123980.0}, + {:date_time=>"2018-11-26T00:00:00.000Z", :adtv=>44572670.90909091}, + {:date_time=>"2018-11-23T00:00:00.000Z", :adtv=>44360389.09090909}, + {:date_time=>"2018-11-21T00:00:00.000Z", :adtv=>45044807.27272727}, + {:date_time=>"2018-11-20T00:00:00.000Z", :adtv=>44938230.0}, + {:date_time=>"2018-11-19T00:00:00.000Z", :adtv=>43356214.09090909}, + {:date_time=>"2018-11-16T00:00:00.000Z", :adtv=>42936325.90909091}, + {:date_time=>"2018-11-15T00:00:00.000Z", :adtv=>42322760.0}, + {:date_time=>"2018-11-14T00:00:00.000Z", :adtv=>41528709.54545455}, + {:date_time=>"2018-11-13T00:00:00.000Z", :adtv=>40152941.81818182}, + {:date_time=>"2018-11-12T00:00:00.000Z", :adtv=>39824262.72727273}, + {:date_time=>"2018-11-09T00:00:00.000Z", :adtv=>39911139.54545455}, + {:date_time=>"2018-11-08T00:00:00.000Z", :adtv=>40218699.09090909}, + {:date_time=>"2018-11-07T00:00:00.000Z", :adtv=>40280851.81818182} + ] + + 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, volume_key: :volume)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('adtv') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Average Daily Trading Volume') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period volume_key)) + end + + it 'Validates options' do + valid_options = { period: 22, volume_key: :close } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(4) + end + end + end +end diff --git a/spec/technical_analysis/indicators/adx_spec.rb b/spec/technical_analysis/indicators/adx_spec.rb new file mode 100644 index 0000000..c8f5d23 --- /dev/null +++ b/spec/technical_analysis/indicators/adx_spec.rb @@ -0,0 +1,92 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "ADX" do + input_data = SpecHelper.get_test_data(:high, :low, :close) + indicator = TechnicalAnalysis::Adx + + describe 'Average Directional Index' do + it 'Calculates ADX (14 day)' do + output = indicator.calculate(input_data, period: 14) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :adx=>46.70506819299097, :di_neg=>33.86727845364526, :di_pos=>18.75156069669946}, + {:date_time=>"2019-01-08T00:00:00.000Z", :adx=>48.08801057392937, :di_neg=>35.92768510004254, :di_pos=>16.527665969119678}, + {:date_time=>"2019-01-07T00:00:00.000Z", :adx=>48.9421751918944, :di_neg=>37.61461933766297, :di_pos=>13.69466932452777}, + {:date_time=>"2019-01-04T00:00:00.000Z", :adx=>49.12087007192141, :di_neg=>38.89182421063156, :di_pos=>13.835071344467728}, + {:date_time=>"2019-01-03T00:00:00.000Z", :adx=>49.24387794011257, :di_neg=>41.7490776508834, :di_pos=>11.582515799387362}, + {:date_time=>"2019-01-02T00:00:00.000Z", :adx=>48.68078018528377, :di_neg=>34.52657095260412, :di_pos=>13.967710052992748}, + {:date_time=>"2018-12-31T00:00:00.000Z", :adx=>49.16434766454135, :di_neg=>33.6937638052496, :di_pos=>14.788354298902822}, + {:date_time=>"2018-12-28T00:00:00.000Z", :adx=>49.94663571159158, :di_neg=>34.986926565583936, :di_pos=>14.325926339774329}, + {:date_time=>"2018-12-27T00:00:00.000Z", :adx=>50.56577696054131, :di_neg=>36.643305871956294, :di_pos=>12.91725635739372}, + {:date_time=>"2018-12-26T00:00:00.000Z", :adx=>50.77292582473832, :di_neg=>39.7700574645743, :di_pos=>14.019478193733173}, + {:date_time=>"2018-12-24T00:00:00.000Z", :adx=>50.9960092169505, :di_neg=>45.054464306949185, :di_pos=>8.701290502977864}, + {:date_time=>"2018-12-21T00:00:00.000Z", :adx=>49.71673521777664, :di_neg=>44.050625611685426, :di_pos=>9.239278206391834}, + {:date_time=>"2018-12-20T00:00:00.000Z", :adx=>48.516140209610576, :di_neg=>41.59440646182574, :di_pos=>10.25145625667257}, + {:date_time=>"2018-12-19T00:00:00.000Z", :adx=>47.59783553446671, :di_neg=>40.351684937107876, :di_pos=>11.15761353425932}, + {:date_time=>"2018-12-18T00:00:00.000Z", :adx=>46.89941641847421, :di_neg=>37.76941014220763, :di_pos=>12.407839995022263}, + {:date_time=>"2018-12-17T00:00:00.000Z", :adx=>46.61906677289316, :di_neg=>39.53590570860133, :di_pos=>12.988161325358192}, + {:date_time=>"2018-12-14T00:00:00.000Z", :adx=>46.31715176995973, :di_neg=>39.11005826049996, :di_pos=>13.935609588410411}, + {:date_time=>"2018-12-13T00:00:00.000Z", :adx=>46.2293890478109, :di_neg=>36.454819093692386, :di_pos=>14.957814674775397}, + {:date_time=>"2018-12-12T00:00:00.000Z", :adx=>46.56913524205153, :di_neg=>38.04051607484649, :di_pos=>14.793647188928983}, + {:date_time=>"2018-12-11T00:00:00.000Z", :adx=>46.76678475437397, :di_neg=>39.55530357861321, :di_pos=>15.225390789013932}, + {:date_time=>"2018-12-10T00:00:00.000Z", :adx=>46.94782223161259, :di_neg=>41.80590109058211, :di_pos=>14.07235093039379}, + {:date_time=>"2018-12-07T00:00:00.000Z", :adx=>46.74133929346658, :di_neg=>39.25064431094187, :di_pos=>15.206157858128352}, + {:date_time=>"2018-12-06T00:00:00.000Z", :adx=>46.94041765413575, :di_neg=>39.72706616642713, :di_pos=>16.369223356492178}, + {:date_time=>"2018-12-04T00:00:00.000Z", :adx=>47.348231515991635, :di_neg=>35.733971514512405, :di_pos=>17.589281738284058}, + {:date_time=>"2018-12-03T00:00:00.000Z", :adx=>48.37288589247055, :di_neg=>33.43673559814864, :di_pos=>19.42230137516784}, + {:date_time=>"2018-11-30T00:00:00.000Z", :adx=>50.05442754589659, :di_neg=>36.030275362364435, :di_pos=>15.306518521740204}, + {:date_time=>"2018-11-29T00:00:00.000Z", :adx=>50.79951939347192, :di_neg=>36.64084491661032, :di_pos=>15.900754457889013}, + {:date_time=>"2018-11-28T00:00:00.000Z", :adx=>51.67073961575017, :di_neg=>38.80264418160902, :di_pos=>15.0920401856127}, + {:date_time=>"2018-11-27T00:00:00.000Z", :adx=>52.261232848231245, :di_neg=>41.98206441213049, :di_pos=>8.75082128345464}, + {:date_time=>"2018-11-26T00:00:00.000Z", :adx=>51.24268374123445, :di_neg=>43.821787689533835, :di_pos=>9.134296699373357}, + {:date_time=>"2018-11-23T00:00:00.000Z", :adx=>50.14578470293022, :di_neg=>44.05855841207259, :di_pos=>9.605544411876144}, + {:date_time=>"2018-11-21T00:00:00.000Z", :adx=>49.0645966148012, :di_neg=>41.4968168736661, :di_pos=>10.08777861559222}, + {:date_time=>"2018-11-20T00:00:00.000Z", :adx=>48.15507276775653, :di_neg=>43.093658309800155, :di_pos=>10.47596701425822}, + {:date_time=>"2018-11-19T00:00:00.000Z", :adx=>47.17558554786226, :di_neg=>37.739607295468765, :di_pos=>11.632425406517202}, + {:date_time=>"2018-11-16T00:00:00.000Z", :adx=>46.73690113316023, :di_neg=>36.39042671024342, :di_pos=>12.707203247013513}, + {:date_time=>"2018-11-15T00:00:00.000Z", :adx=>46.621508959519026, :di_neg=>38.52265796697687, :di_pos=>10.26180913708443} + ] + + 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)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('adx') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Average Directional Index') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period)) + end + + it 'Validates options' do + valid_options = { period: 22 } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(8) + end + end + end +end diff --git a/spec/technical_analysis/indicators/ao_spec.rb b/spec/technical_analysis/indicators/ao_spec.rb new file mode 100644 index 0000000..c101876 --- /dev/null +++ b/spec/technical_analysis/indicators/ao_spec.rb @@ -0,0 +1,86 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "AO" do + input_data = SpecHelper.get_test_data(:high, :low) + indicator = TechnicalAnalysis::Ao + + describe 'Awesome Oscillator' do + it 'Calculates AO (5 day short period, 34 day long period)' do + output = indicator.calculate(input_data, short_period: 5, long_period: 34) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :ao=>-17.518757058823525}, + {:date_time=>"2019-01-08T00:00:00.000Z", :ao=>-17.8071908823529}, + {:date_time=>"2019-01-07T00:00:00.000Z", :ao=>-17.41204382352936}, + {:date_time=>"2019-01-04T00:00:00.000Z", :ao=>-16.838043823529347}, + {:date_time=>"2019-01-03T00:00:00.000Z", :ao=>-16.804919117647017}, + {:date_time=>"2019-01-02T00:00:00.000Z", :ao=>-16.73956617647059}, + {:date_time=>"2018-12-31T00:00:00.000Z", :ao=>-19.633272058823536}, + {:date_time=>"2018-12-28T00:00:00.000Z", :ao=>-21.924007352941203}, + {:date_time=>"2018-12-27T00:00:00.000Z", :ao=>-22.97706617647063}, + {:date_time=>"2018-12-26T00:00:00.000Z", :ao=>-22.471330882352987}, + {:date_time=>"2018-12-24T00:00:00.000Z", :ao=>-21.124477941176536}, + {:date_time=>"2018-12-21T00:00:00.000Z", :ao=>-19.609007352941205}, + {:date_time=>"2018-12-20T00:00:00.000Z", :ao=>-18.88406617647061}, + {:date_time=>"2018-12-19T00:00:00.000Z", :ao=>-18.17277205882357}, + {:date_time=>"2018-12-18T00:00:00.000Z", :ao=>-18.17262500000004}, + {:date_time=>"2018-12-17T00:00:00.000Z", :ao=>-18.865919117647138}, + {:date_time=>"2018-12-14T00:00:00.000Z", :ao=>-20.12868382352943}, + {:date_time=>"2018-12-13T00:00:00.000Z", :ao=>-20.811713235294178}, + {:date_time=>"2018-12-12T00:00:00.000Z", :ao=>-21.92503676470585}, + {:date_time=>"2018-12-11T00:00:00.000Z", :ao=>-21.579664411764696}, + {:date_time=>"2018-12-10T00:00:00.000Z", :ao=>-20.365870294117656}, + {:date_time=>"2018-12-07T00:00:00.000Z", :ao=>-19.51995852941178}, + {:date_time=>"2018-12-06T00:00:00.000Z", :ao=>-19.071752647058815}, + {:date_time=>"2018-12-04T00:00:00.000Z", :ao=>-19.392987941176415}, + {:date_time=>"2018-12-03T00:00:00.000Z", :ao=>-21.886519117646998}, + {:date_time=>"2018-11-30T00:00:00.000Z", :ao=>-25.05331323529407}, + {:date_time=>"2018-11-29T00:00:00.000Z", :ao=>-27.130989705882314}, + {:date_time=>"2018-11-28T00:00:00.000Z", :ao=>-28.547813235294086}, + {:date_time=>"2018-11-27T00:00:00.000Z", :ao=>-29.739166176470576}, + {:date_time=>"2018-11-26T00:00:00.000Z", :ao=>-28.26261029411762} + ] + + expect(normalized_output).to eq(expected_output) + end + + it "Throws exception if not enough data" do + expect {indicator.calculate(input_data, long_period: input_data.size+1)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('ao') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Awesome Oscillator') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(short_period long_period)) + end + + it 'Validates options' do + valid_options = { short_period: 5, long_period: 34 } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { long_period: 20 } + expect(indicator.min_data_size(options)).to eq(20) + end + end + end +end diff --git a/spec/technical_analysis/indicators/atr_spec.rb b/spec/technical_analysis/indicators/atr_spec.rb new file mode 100644 index 0000000..488ca8f --- /dev/null +++ b/spec/technical_analysis/indicators/atr_spec.rb @@ -0,0 +1,105 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "ATR" do + input_data = SpecHelper.get_test_data(:high, :low, :close) + indicator = TechnicalAnalysis::Atr + + describe 'Average True Range' do + it 'Calculates ATR (14 day)' do + output = indicator.calculate(input_data, period: 14) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :atr=>6.103013600253306}, + {:date_time=>"2019-01-08T00:00:00.000Z", :atr=>6.195553107965099}, + {:date_time=>"2019-01-07T00:00:00.000Z", :atr=>6.3729033470393395}, + {:date_time=>"2019-01-04T00:00:00.000Z", :atr=>6.637742066042365}, + {:date_time=>"2019-01-03T00:00:00.000Z", :atr=>6.6591145326610075}, + {:date_time=>"2019-01-02T00:00:00.000Z", :atr=>5.946738727481086}, + {:date_time=>"2018-12-31T00:00:00.000Z", :atr=>6.048795552671939}, + {:date_time=>"2018-12-28T00:00:00.000Z", :atr=>6.273318287492855}, + {:date_time=>"2018-12-27T00:00:00.000Z", :atr=>6.450496617299998}, + {:date_time=>"2018-12-26T00:00:00.000Z", :atr=>6.400534818630767}, + {:date_time=>"2018-12-24T00:00:00.000Z", :atr=>6.084422112371596}, + {:date_time=>"2018-12-21T00:00:00.000Z", :atr=>6.170916121015564}, + {:date_time=>"2018-12-20T00:00:00.000Z", :atr=>5.989448130324453}, + {:date_time=>"2018-12-19T00:00:00.000Z", :atr=>5.926328755734025}, + {:date_time=>"2018-12-18T00:00:00.000Z", :atr=>5.739123275405874}, + {:date_time=>"2018-12-17T00:00:00.000Z", :atr=>5.904440450437095}, + {:date_time=>"2018-12-14T00:00:00.000Z", :atr=>5.926320485086102}, + {:date_time=>"2018-12-13T00:00:00.000Z", :atr=>5.946037445477343}, + {:date_time=>"2018-12-12T00:00:00.000Z", :atr=>6.1365018643602145}, + {:date_time=>"2018-12-11T00:00:00.000Z", :atr=>6.355463546234078}, + {:date_time=>"2018-12-10T00:00:00.000Z", :atr=>6.475883819021315}, + {:date_time=>"2018-12-07T00:00:00.000Z", :atr=>6.4540287281768025}, + {:date_time=>"2018-12-06T00:00:00.000Z", :atr=>6.456646322651943}, + {:date_time=>"2018-12-04T00:00:00.000Z", :atr=>6.471003732086706}, + {:date_time=>"2018-12-03T00:00:00.000Z", :atr=>6.311080942247224}, + {:date_time=>"2018-11-30T00:00:00.000Z", :atr=>6.307317937804704}, + {:date_time=>"2018-11-29T00:00:00.000Z", :atr=>6.538650086866604}, + {:date_time=>"2018-11-28T00:00:00.000Z", :atr=>6.649315478164034}, + {:date_time=>"2018-11-27T00:00:00.000Z", :atr=>6.618493591868961}, + {:date_time=>"2018-11-26T00:00:00.000Z", :atr=>6.828377714320418}, + {:date_time=>"2018-11-23T00:00:00.000Z", :atr=>6.9928683077296805}, + {:date_time=>"2018-11-21T00:00:00.000Z", :atr=>7.1707812544781175}, + {:date_time=>"2018-11-20T00:00:00.000Z", :atr=>7.43622596636105}, + {:date_time=>"2018-11-19T00:00:00.000Z", :atr=>7.212089502234975}, + {:date_time=>"2018-11-16T00:00:00.000Z", :atr=>7.109942540868436}, + {:date_time=>"2018-11-15T00:00:00.000Z", :atr=>7.233053505550622}, + {:date_time=>"2018-11-14T00:00:00.000Z", :atr=>7.391749929054517}, + {:date_time=>"2018-11-13T00:00:00.000Z", :atr=>7.302653769751019}, + {:date_time=>"2018-11-12T00:00:00.000Z", :atr=>7.423634828962634}, + {:date_time=>"2018-11-09T00:00:00.000Z", :atr=>7.173145200421298}, + {:date_time=>"2018-11-08T00:00:00.000Z", :atr=>7.244925600453705}, + {:date_time=>"2018-11-07T00:00:00.000Z", :atr=>7.542996800488605}, + {:date_time=>"2018-11-06T00:00:00.000Z", :atr=>7.63938116975696}, + {:date_time=>"2018-11-05T00:00:00.000Z", :atr=>7.98625664435365}, + {:date_time=>"2018-11-02T00:00:00.000Z", :atr=>7.884430232380852}, + {:date_time=>"2018-11-01T00:00:00.000Z", :atr=>7.199386404102457}, + {:date_time=>"2018-10-31T00:00:00.000Z", :atr=>7.326262281341107}, + {:date_time=>"2018-10-30T00:00:00.000Z", :atr=>7.339820918367347}, + {:date_time=>"2018-10-29T00:00:00.000Z", :atr=>7.449807142857144} + ] + + 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)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('atr') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Average True Range') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period)) + end + + it 'Validates options' do + valid_options = { period: 22 } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(5) + end + end + end +end diff --git a/spec/technical_analysis/indicators/bb_spec.rb b/spec/technical_analysis/indicators/bb_spec.rb new file mode 100644 index 0000000..3e3f9eb --- /dev/null +++ b/spec/technical_analysis/indicators/bb_spec.rb @@ -0,0 +1,100 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "BB" do + input_data = SpecHelper.get_test_data(:close) + 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) + + 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} + ] + + 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 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(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 valid options' do + valid_options = indicator.valid_options + expect(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 '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) + end + end + end +end diff --git a/spec/technical_analysis/indicators/cci_spec.rb b/spec/technical_analysis/indicators/cci_spec.rb new file mode 100644 index 0000000..6b6c146 --- /dev/null +++ b/spec/technical_analysis/indicators/cci_spec.rb @@ -0,0 +1,100 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "CCI" do + input_data = SpecHelper.get_test_data(:high, :low, :close) + indicator = TechnicalAnalysis::Cci + + describe 'Commodity Channel Index' do + it 'Calculates CCI (20 day)' do + output = indicator.calculate(input_data, period: 20, constant: 0.015) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :cci=>-48.14847062019609}, + {:date_time=>"2019-01-08T00:00:00.000Z", :cci=>-72.7408611895969}, + {:date_time=>"2019-01-07T00:00:00.000Z", :cci=>-103.45330536502108}, + {:date_time=>"2019-01-04T00:00:00.000Z", :cci=>-119.01911861945885}, + {:date_time=>"2019-01-03T00:00:00.000Z", :cci=>-162.69069349674928}, + {:date_time=>"2019-01-02T00:00:00.000Z", :cci=>-62.42850721332871}, + {:date_time=>"2018-12-31T00:00:00.000Z", :cci=>-62.94237272962061}, + {:date_time=>"2018-12-28T00:00:00.000Z", :cci=>-82.72457326337747}, + {:date_time=>"2018-12-27T00:00:00.000Z", :cci=>-109.82143904961333}, + {:date_time=>"2018-12-26T00:00:00.000Z", :cci=>-130.89740843118958}, + {:date_time=>"2018-12-24T00:00:00.000Z", :cci=>-205.86763488625635}, + {:date_time=>"2018-12-21T00:00:00.000Z", :cci=>-204.3001270602558}, + {:date_time=>"2018-12-20T00:00:00.000Z", :cci=>-175.655233047097}, + {:date_time=>"2018-12-19T00:00:00.000Z", :cci=>-145.86611021295013}, + {:date_time=>"2018-12-18T00:00:00.000Z", :cci=>-108.36683776743406}, + {:date_time=>"2018-12-17T00:00:00.000Z", :cci=>-123.22931274779911}, + {:date_time=>"2018-12-14T00:00:00.000Z", :cci=>-114.79798652194032}, + {:date_time=>"2018-12-13T00:00:00.000Z", :cci=>-77.37092752385674}, + {:date_time=>"2018-12-12T00:00:00.000Z", :cci=>-93.5619842292165}, + {:date_time=>"2018-12-11T00:00:00.000Z", :cci=>-105.1287325078536}, + {:date_time=>"2018-12-10T00:00:00.000Z", :cci=>-118.83020945347108}, + {:date_time=>"2018-12-07T00:00:00.000Z", :cci=>-102.13101656480995}, + {:date_time=>"2018-12-06T00:00:00.000Z", :cci=>-87.66403875835518}, + {:date_time=>"2018-12-04T00:00:00.000Z", :cci=>-60.69535791887537}, + {:date_time=>"2018-12-03T00:00:00.000Z", :cci=>-32.43699792850786}, + {:date_time=>"2018-11-30T00:00:00.000Z", :cci=>-68.8774176049022}, + {:date_time=>"2018-11-29T00:00:00.000Z", :cci=>-67.42425893465922}, + {:date_time=>"2018-11-28T00:00:00.000Z", :cci=>-79.16162769465076}, + {:date_time=>"2018-11-27T00:00:00.000Z", :cci=>-117.06521007698558}, + {:date_time=>"2018-11-26T00:00:00.000Z", :cci=>-130.57777507098388}, + {:date_time=>"2018-11-23T00:00:00.000Z", :cci=>-145.81148745067057}, + {:date_time=>"2018-11-21T00:00:00.000Z", :cci=>-143.70909871161282}, + {:date_time=>"2018-11-20T00:00:00.000Z", :cci=>-166.092016017792}, + {:date_time=>"2018-11-19T00:00:00.000Z", :cci=>-129.79321417259732}, + {:date_time=>"2018-11-16T00:00:00.000Z", :cci=>-109.8703962546515}, + {:date_time=>"2018-11-15T00:00:00.000Z", :cci=>-144.29736082195598}, + {:date_time=>"2018-11-14T00:00:00.000Z", :cci=>-176.53554346437872}, + {:date_time=>"2018-11-13T00:00:00.000Z", :cci=>-170.11758119513328}, + {:date_time=>"2018-11-12T00:00:00.000Z", :cci=>-179.88996064655686}, + {:date_time=>"2018-11-09T00:00:00.000Z", :cci=>-121.3842764199079}, + {:date_time=>"2018-11-08T00:00:00.000Z", :cci=>-88.26723659204252}, + {:date_time=>"2018-11-07T00:00:00.000Z", :cci=>-107.40409620543349}, + {:date_time=>"2018-11-06T00:00:00.000Z", :cci=>-198.71238766942466}, + {:date_time=>"2018-11-05T00:00:00.000Z", :cci=>-281.5255251804358} + ] + + 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)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('cci') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Commodity Channel Index') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period constant)) + end + + it 'Validates options' do + valid_options = { period: 22 } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(4) + end + end + end +end diff --git a/spec/technical_analysis/indicators/cmf_spec.rb b/spec/technical_analysis/indicators/cmf_spec.rb new file mode 100644 index 0000000..a0d104e --- /dev/null +++ b/spec/technical_analysis/indicators/cmf_spec.rb @@ -0,0 +1,100 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "CMF" do + input_data = SpecHelper.get_test_data(:high, :low, :close, :volume) + indicator = TechnicalAnalysis::Cmf + + describe 'Chaikin Money Flow' do + it 'Calculates CMF (20 day)' do + output = indicator.calculate(input_data, period: 20) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :cmf=>-0.14148236474171028}, + {:date_time=>"2019-01-08T00:00:00.000Z", :cmf=>-0.10900349402409147}, + {:date_time=>"2019-01-07T00:00:00.000Z", :cmf=>-0.16209459049078995}, + {:date_time=>"2019-01-04T00:00:00.000Z", :cmf=>-0.14338098793972473}, + {:date_time=>"2019-01-03T00:00:00.000Z", :cmf=>-0.23384083275693518}, + {:date_time=>"2019-01-02T00:00:00.000Z", :cmf=>-0.1171779554311941}, + {:date_time=>"2018-12-31T00:00:00.000Z", :cmf=>-0.1421967277504723}, + {:date_time=>"2018-12-28T00:00:00.000Z", :cmf=>-0.14870217725852253}, + {:date_time=>"2018-12-27T00:00:00.000Z", :cmf=>-0.09771633750350824}, + {:date_time=>"2018-12-26T00:00:00.000Z", :cmf=>-0.11185040161958644}, + {:date_time=>"2018-12-24T00:00:00.000Z", :cmf=>-0.13433878933016613}, + {:date_time=>"2018-12-21T00:00:00.000Z", :cmf=>-0.12311876857928804}, + {:date_time=>"2018-12-20T00:00:00.000Z", :cmf=>-0.08053545285645361}, + {:date_time=>"2018-12-19T00:00:00.000Z", :cmf=>-0.07883316711924936}, + {:date_time=>"2018-12-18T00:00:00.000Z", :cmf=>-0.08160027269601758}, + {:date_time=>"2018-12-17T00:00:00.000Z", :cmf=>-0.0635610565928198}, + {:date_time=>"2018-12-14T00:00:00.000Z", :cmf=>0.008829457014340984}, + {:date_time=>"2018-12-13T00:00:00.000Z", :cmf=>-0.005177700749048257}, + {:date_time=>"2018-12-12T00:00:00.000Z", :cmf=>-0.041279122544240626}, + {:date_time=>"2018-12-11T00:00:00.000Z", :cmf=>-0.05299093511730075}, + {:date_time=>"2018-12-10T00:00:00.000Z", :cmf=>-0.02994416009907359}, + {:date_time=>"2018-12-07T00:00:00.000Z", :cmf=>-0.09289096149170357}, + {:date_time=>"2018-12-06T00:00:00.000Z", :cmf=>-0.008556645577315109}, + {:date_time=>"2018-12-04T00:00:00.000Z", :cmf=>-0.044697234566322144}, + {:date_time=>"2018-12-03T00:00:00.000Z", :cmf=>0.006195178249247199}, + {:date_time=>"2018-11-30T00:00:00.000Z", :cmf=>-0.08707816577338795}, + {:date_time=>"2018-11-29T00:00:00.000Z", :cmf=>-0.02808273355873217}, + {:date_time=>"2018-11-28T00:00:00.000Z", :cmf=>-0.008559410227273146}, + {:date_time=>"2018-11-27T00:00:00.000Z", :cmf=>-0.039334642581608396}, + {:date_time=>"2018-11-26T00:00:00.000Z", :cmf=>-0.07707518115228632}, + {:date_time=>"2018-11-23T00:00:00.000Z", :cmf=>-0.12097582444999905}, + {:date_time=>"2018-11-21T00:00:00.000Z", :cmf=>-0.0864519745264971}, + {:date_time=>"2018-11-20T00:00:00.000Z", :cmf=>-0.09451983283878523}, + {:date_time=>"2018-11-19T00:00:00.000Z", :cmf=>-0.02107439975862456}, + {:date_time=>"2018-11-16T00:00:00.000Z", :cmf=>0.004132959703610273}, + {:date_time=>"2018-11-15T00:00:00.000Z", :cmf=>-0.016262278292090777}, + {:date_time=>"2018-11-14T00:00:00.000Z", :cmf=>-0.06224056395025631}, + {:date_time=>"2018-11-13T00:00:00.000Z", :cmf=>-0.00325588286845078}, + {:date_time=>"2018-11-12T00:00:00.000Z", :cmf=>0.06492697386482489}, + {:date_time=>"2018-11-09T00:00:00.000Z", :cmf=>0.08638139363431603}, + {:date_time=>"2018-11-08T00:00:00.000Z", :cmf=>0.11517576782439708}, + {:date_time=>"2018-11-07T00:00:00.000Z", :cmf=>0.0839939173853672}, + {:date_time=>"2018-11-06T00:00:00.000Z", :cmf=>-0.002328367522189358}, + {:date_time=>"2018-11-05T00:00:00.000Z", :cmf=>0.010519910116535688} + ] + + 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)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('cmf') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Chaikin Money Flow') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period)) + end + + it 'Validates options' do + valid_options = { period: 22 } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(4) + end + end + end +end diff --git a/spec/technical_analysis/indicators/cr_spec.rb b/spec/technical_analysis/indicators/cr_spec.rb new file mode 100644 index 0000000..a996149 --- /dev/null +++ b/spec/technical_analysis/indicators/cr_spec.rb @@ -0,0 +1,119 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "CR" do + input_data = SpecHelper.get_test_data(:close) + indicator = TechnicalAnalysis::Cr + + describe 'Cumulative Return' do + it 'Calculates CR' do + output = indicator.calculate(input_data, price_key: :close) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :cr=>-0.3242385507118614}, + {:date_time=>"2019-01-08T00:00:00.000Z", :cr=>-0.33552254595142594}, + {:date_time=>"2019-01-07T00:00:00.000Z", :cr=>-0.34795257195750867}, + {:date_time=>"2019-01-04T00:00:00.000Z", :cr=>-0.34649799444615864}, + {:date_time=>"2019-01-03T00:00:00.000Z", :cr=>-0.3732534050337198}, + {:date_time=>"2019-01-02T00:00:00.000Z", :cr=>-0.30391854365936444}, + {:date_time=>"2018-12-31T00:00:00.000Z", :cr=>-0.30471194957464626}, + {:date_time=>"2018-12-28T00:00:00.000Z", :cr=>-0.3113677436417332}, + {:date_time=>"2018-12-27T00:00:00.000Z", :cr=>-0.3117203684929695}, + {:date_time=>"2018-12-26T00:00:00.000Z", :cr=>-0.3072244016397056}, + {:date_time=>"2018-12-24T00:00:00.000Z", :cr=>-0.35280116366200903}, + {:date_time=>"2018-12-21T00:00:00.000Z", :cr=>-0.3356107021642351}, + {:date_time=>"2018-12-20T00:00:00.000Z", :cr=>-0.3087230572574602}, + {:date_time=>"2018-12-19T00:00:00.000Z", :cr=>-0.29082734605721344}, + {:date_time=>"2018-12-18T00:00:00.000Z", :cr=>-0.2679948869396571}, + {:date_time=>"2018-12-17T00:00:00.000Z", :cr=>-0.277383523603826}, + {:date_time=>"2018-12-14T00:00:00.000Z", :cr=>-0.2705954952175255}, + {:date_time=>"2018-12-13T00:00:00.000Z", :cr=>-0.2464847710142373}, + {:date_time=>"2018-12-12T00:00:00.000Z", :cr=>-0.2546392206990788}, + {:date_time=>"2018-12-11T00:00:00.000Z", :cr=>-0.2567108917000926}, + {:date_time=>"2018-12-10T00:00:00.000Z", :cr=>-0.2524353153788514}, + {:date_time=>"2018-12-07T00:00:00.000Z", :cr=>-0.25732798518975625}, + {:date_time=>"2018-12-06T00:00:00.000Z", :cr=>-0.22986732489972234}, + {:date_time=>"2018-12-04T00:00:00.000Z", :cr=>-0.2211839379380262}, + {:date_time=>"2018-12-03T00:00:00.000Z", :cr=>-0.185348437431128}, + {:date_time=>"2018-11-30T00:00:00.000Z", :cr=>-0.2128531758275664}, + {:date_time=>"2018-11-29T00:00:00.000Z", :cr=>-0.20857759950632518}, + {:date_time=>"2018-11-28T00:00:00.000Z", :cr=>-0.20245074271609295}, + {:date_time=>"2018-11-27T00:00:00.000Z", :cr=>-0.23198307400714063}, + {:date_time=>"2018-11-26T00:00:00.000Z", :cr=>-0.2303081059637678}, + {:date_time=>"2018-11-23T00:00:00.000Z", :cr=>-0.24057830475602773}, + {:date_time=>"2018-11-21T00:00:00.000Z", :cr=>-0.22078723498038524}, + {:date_time=>"2018-11-20T00:00:00.000Z", :cr=>-0.21990567285229431}, + {:date_time=>"2018-11-19T00:00:00.000Z", :cr=>-0.18076431436505483}, + {:date_time=>"2018-11-16T00:00:00.000Z", :cr=>-0.14695640675276592}, + {:date_time=>"2018-11-15T00:00:00.000Z", :cr=>-0.15630096531053028}, + {:date_time=>"2018-11-14T00:00:00.000Z", :cr=>-0.17662097236302726}, + {:date_time=>"2018-11-13T00:00:00.000Z", :cr=>-0.15268656058535732}, + {:date_time=>"2018-11-12T00:00:00.000Z", :cr=>-0.14413540794287485}, + {:date_time=>"2018-11-09T00:00:00.000Z", :cr=>-0.09873495834618948}, + {:date_time=>"2018-11-08T00:00:00.000Z", :cr=>-0.08101555957156079}, + {:date_time=>"2018-11-07T00:00:00.000Z", :cr=>-0.07458015603649674}, + {:date_time=>"2018-11-06T00:00:00.000Z", :cr=>-0.10182042579450784}, + {:date_time=>"2018-11-05T00:00:00.000Z", :cr=>-0.11142945299069952}, + {:date_time=>"2018-11-02T00:00:00.000Z", :cr=>-0.0854674483184203}, + {:date_time=>"2018-11-01T00:00:00.000Z", :cr=>-0.020496319478115244}, + {:date_time=>"2018-10-31T00:00:00.000Z", :cr=>-0.035306563230043594}, + {:date_time=>"2018-10-30T00:00:00.000Z", :cr=>-0.05981399039097277}, + {:date_time=>"2018-10-29T00:00:00.000Z", :cr=>-0.06448626966985496}, + {:date_time=>"2018-10-26T00:00:00.000Z", :cr=>-0.046590558469608113}, + {:date_time=>"2018-10-25T00:00:00.000Z", :cr=>-0.031163221228016014}, + {:date_time=>"2018-10-24T00:00:00.000Z", :cr=>-0.05192400934455856}, + {:date_time=>"2018-10-23T00:00:00.000Z", :cr=>-0.018248336051483294}, + {:date_time=>"2018-10-22T00:00:00.000Z", :cr=>-0.027416582183629384}, + {:date_time=>"2018-10-19T00:00:00.000Z", :cr=>-0.03332304844183895}, + {:date_time=>"2018-10-18T00:00:00.000Z", :cr=>-0.04782474544893549}, + {:date_time=>"2018-10-17T00:00:00.000Z", :cr=>-0.025036364437783783}, + {:date_time=>"2018-10-16T00:00:00.000Z", :cr=>-0.02080486622294706}, + {:date_time=>"2018-10-15T00:00:00.000Z", :cr=>-0.041918279190725924}, + {:date_time=>"2018-10-12T00:00:00.000Z", :cr=>-0.02098117864856522}, + {:date_time=>"2018-10-11T00:00:00.000Z", :cr=>-0.054745008154449756}, + {:date_time=>"2018-10-10T00:00:00.000Z", :cr=>-0.04632608983118081}, + {:date_time=>"2018-10-09T00:00:00.000Z", :cr=>0.0} + ] + + expect(normalized_output).to eq(expected_output) + end + + it "Throws exception if not enough data" do + expect {indicator.calculate([], 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('cr') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Cumulative Return') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(price_key)) + end + + it 'Validates options' do + valid_options = { price_key: :close } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = {} + expect(indicator.min_data_size(options)).to eq(1) + end + end + end +end diff --git a/spec/technical_analysis/indicators/dc_spec.rb b/spec/technical_analysis/indicators/dc_spec.rb new file mode 100644 index 0000000..79c8e12 --- /dev/null +++ b/spec/technical_analysis/indicators/dc_spec.rb @@ -0,0 +1,100 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "DC" do + input_data = SpecHelper.get_test_data(:close) + indicator = TechnicalAnalysis::Dc + + describe 'Donchian Channel' do + it 'Calculates DC (20 day)' do + output = indicator.calculate(input_data, period: 20, price_key: :close) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :lower_bound=>142.19, :upper_bound=>170.95}, + {:date_time=>"2019-01-08T00:00:00.000Z", :lower_bound=>142.19, :upper_bound=>170.95}, + {:date_time=>"2019-01-07T00:00:00.000Z", :lower_bound=>142.19, :upper_bound=>170.95}, + {:date_time=>"2019-01-04T00:00:00.000Z", :lower_bound=>142.19, :upper_bound=>174.72}, + {:date_time=>"2019-01-03T00:00:00.000Z", :lower_bound=>142.19, :upper_bound=>176.69}, + {:date_time=>"2019-01-02T00:00:00.000Z", :lower_bound=>146.83, :upper_bound=>184.82}, + {:date_time=>"2018-12-31T00:00:00.000Z", :lower_bound=>146.83, :upper_bound=>184.82}, + {:date_time=>"2018-12-28T00:00:00.000Z", :lower_bound=>146.83, :upper_bound=>184.82}, + {:date_time=>"2018-12-27T00:00:00.000Z", :lower_bound=>146.83, :upper_bound=>184.82}, + {:date_time=>"2018-12-26T00:00:00.000Z", :lower_bound=>146.83, :upper_bound=>184.82}, + {:date_time=>"2018-12-24T00:00:00.000Z", :lower_bound=>146.83, :upper_bound=>184.82}, + {:date_time=>"2018-12-21T00:00:00.000Z", :lower_bound=>150.73, :upper_bound=>184.82}, + {:date_time=>"2018-12-20T00:00:00.000Z", :lower_bound=>156.83, :upper_bound=>184.82}, + {:date_time=>"2018-12-19T00:00:00.000Z", :lower_bound=>160.89, :upper_bound=>184.82}, + {:date_time=>"2018-12-18T00:00:00.000Z", :lower_bound=>163.94, :upper_bound=>185.86}, + {:date_time=>"2018-12-17T00:00:00.000Z", :lower_bound=>163.94, :upper_bound=>193.53}, + {:date_time=>"2018-12-14T00:00:00.000Z", :lower_bound=>165.48, :upper_bound=>193.53}, + {:date_time=>"2018-12-13T00:00:00.000Z", :lower_bound=>168.49, :upper_bound=>193.53}, + {:date_time=>"2018-12-12T00:00:00.000Z", :lower_bound=>168.49, :upper_bound=>193.53}, + {:date_time=>"2018-12-11T00:00:00.000Z", :lower_bound=>168.49, :upper_bound=>194.17}, + {:date_time=>"2018-12-10T00:00:00.000Z", :lower_bound=>168.49, :upper_bound=>204.47}, + {:date_time=>"2018-12-07T00:00:00.000Z", :lower_bound=>168.49, :upper_bound=>208.49}, + {:date_time=>"2018-12-06T00:00:00.000Z", :lower_bound=>172.29, :upper_bound=>209.95}, + {:date_time=>"2018-12-04T00:00:00.000Z", :lower_bound=>172.29, :upper_bound=>209.95}, + {:date_time=>"2018-12-03T00:00:00.000Z", :lower_bound=>172.29, :upper_bound=>209.95}, + {:date_time=>"2018-11-30T00:00:00.000Z", :lower_bound=>172.29, :upper_bound=>209.95}, + {:date_time=>"2018-11-29T00:00:00.000Z", :lower_bound=>172.29, :upper_bound=>222.22}, + {:date_time=>"2018-11-28T00:00:00.000Z", :lower_bound=>172.29, :upper_bound=>222.22}, + {:date_time=>"2018-11-27T00:00:00.000Z", :lower_bound=>172.29, :upper_bound=>222.22}, + {:date_time=>"2018-11-26T00:00:00.000Z", :lower_bound=>172.29, :upper_bound=>222.22}, + {:date_time=>"2018-11-23T00:00:00.000Z", :lower_bound=>172.29, :upper_bound=>222.22}, + {:date_time=>"2018-11-21T00:00:00.000Z", :lower_bound=>176.78, :upper_bound=>222.22}, + {:date_time=>"2018-11-20T00:00:00.000Z", :lower_bound=>176.98, :upper_bound=>222.22}, + {:date_time=>"2018-11-19T00:00:00.000Z", :lower_bound=>185.86, :upper_bound=>222.73}, + {:date_time=>"2018-11-16T00:00:00.000Z", :lower_bound=>186.8, :upper_bound=>222.73}, + {:date_time=>"2018-11-15T00:00:00.000Z", :lower_bound=>186.8, :upper_bound=>222.73}, + {:date_time=>"2018-11-14T00:00:00.000Z", :lower_bound=>186.8, :upper_bound=>222.73}, + {:date_time=>"2018-11-13T00:00:00.000Z", :lower_bound=>192.23, :upper_bound=>222.73}, + {:date_time=>"2018-11-12T00:00:00.000Z", :lower_bound=>194.17, :upper_bound=>222.73}, + {:date_time=>"2018-11-09T00:00:00.000Z", :lower_bound=>201.59, :upper_bound=>222.73}, + {:date_time=>"2018-11-08T00:00:00.000Z", :lower_bound=>201.59, :upper_bound=>222.73}, + {:date_time=>"2018-11-07T00:00:00.000Z", :lower_bound=>201.59, :upper_bound=>222.73}, + {:date_time=>"2018-11-06T00:00:00.000Z", :lower_bound=>201.59, :upper_bound=>222.73}, + {:date_time=>"2018-11-05T00:00:00.000Z", :lower_bound=>201.59, :upper_bound=>226.87} + ] + + 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 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('dc') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Donchian Channel') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period price_key)) + end + + it 'Validates options' do + valid_options = { period: 22, price_key: :close } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(4) + end + end + end +end diff --git a/spec/technical_analysis/indicators/dlr_spec.rb b/spec/technical_analysis/indicators/dlr_spec.rb new file mode 100644 index 0000000..f4453bf --- /dev/null +++ b/spec/technical_analysis/indicators/dlr_spec.rb @@ -0,0 +1,119 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "DLR" do + input_data = SpecHelper.get_test_data(:close) + indicator = TechnicalAnalysis::Dlr + + describe 'Daily Log Return' do + it 'Calculates Daily Log Return' do + output = indicator.calculate(input_data, price_key: :close) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :dlr=>0.01683917971506794}, + {:date_time=>"2019-01-08T00:00:00.000Z", :dlr=>0.01888364670315034}, + {:date_time=>"2019-01-07T00:00:00.000Z", :dlr=>-0.0022283003244291597}, + {:date_time=>"2019-01-04T00:00:00.000Z", :dlr=>0.04180329782029066}, + {:date_time=>"2019-01-03T00:00:00.000Z", :dlr=>-0.10492438427688831}, + {:date_time=>"2019-01-02T00:00:00.000Z", :dlr=>0.0011404677153263752}, + {:date_time=>"2018-12-31T00:00:00.000Z", :dlr=>0.009618827546033224}, + {:date_time=>"2018-12-28T00:00:00.000Z", :dlr=>0.0005121966947240164}, + {:date_time=>"2018-12-27T00:00:00.000Z", :dlr=>-0.006510938359150324}, + {:date_time=>"2018-12-26T00:00:00.000Z", :dlr=>0.06805256711339175}, + {:date_time=>"2018-12-24T00:00:00.000Z", :dlr=>-0.026214701847591528}, + {:date_time=>"2018-12-21T00:00:00.000Z", :dlr=>-0.039672259323895565}, + {:date_time=>"2018-12-20T00:00:00.000Z", :dlr=>-0.02555848551661011}, + {:date_time=>"2018-12-19T00:00:00.000Z", :dlr=>-0.03168848455571821}, + {:date_time=>"2018-12-18T00:00:00.000Z", :dlr=>0.012908878993777004}, + {:date_time=>"2018-12-17T00:00:00.000Z", :dlr=>-0.0093498343683261}, + {:date_time=>"2018-12-14T00:00:00.000Z", :dlr=>-0.032520774492099586}, + {:date_time=>"2018-12-13T00:00:00.000Z", :dlr=>0.010880860180775654}, + {:date_time=>"2018-12-12T00:00:00.000Z", :dlr=>0.002783290222441408}, + {:date_time=>"2018-12-11T00:00:00.000Z", :dlr=>-0.00573575767570937}, + {:date_time=>"2018-12-10T00:00:00.000Z", :dlr=>0.00656632250750863}, + {:date_time=>"2018-12-07T00:00:00.000Z", :dlr=>-0.03630829170624626}, + {:date_time=>"2018-12-06T00:00:00.000Z", :dlr=>-0.011212092072018474}, + {:date_time=>"2018-12-04T00:00:00.000Z", :dlr=>-0.044985593831335616}, + {:date_time=>"2018-12-03T00:00:00.000Z", :dlr=>0.034345698370823956}, + {:date_time=>"2018-11-30T00:00:00.000Z", :dlr=>-0.005417040583021469}, + {:date_time=>"2018-11-29T00:00:00.000Z", :dlr=>-0.007711763925503673}, + {:date_time=>"2018-11-28T00:00:00.000Z", :dlr=>0.037731825412945244}, + {:date_time=>"2018-11-27T00:00:00.000Z", :dlr=>-0.002178525198011193}, + {:date_time=>"2018-11-26T00:00:00.000Z", :dlr=>0.013433080838346925}, + {:date_time=>"2018-11-23T00:00:00.000Z", :dlr=>-0.02572691808848715}, + {:date_time=>"2018-11-21T00:00:00.000Z", :dlr=>-0.0011307102064021337}, + {:date_time=>"2018-11-20T00:00:00.000Z", :dlr=>-0.04895697028998929}, + {:date_time=>"2018-11-19T00:00:00.000Z", :dlr=>-0.04043883708306672}, + {:date_time=>"2018-11-16T00:00:00.000Z", :dlr=>0.011014814954241668}, + {:date_time=>"2018-11-15T00:00:00.000Z", :dlr=>0.024379198463276127}, + {:date_time=>"2018-11-14T00:00:00.000Z", :dlr=>-0.028654045970026604}, + {:date_time=>"2018-11-13T00:00:00.000Z", :dlr=>-0.010041492241593453}, + {:date_time=>"2018-11-12T00:00:00.000Z", :dlr=>-0.051687201448600596}, + {:date_time=>"2018-11-09T00:00:00.000Z", :dlr=>-0.01946981300300202}, + {:date_time=>"2018-11-08T00:00:00.000Z", :dlr=>-0.006978328672237313}, + {:date_time=>"2018-11-07T00:00:00.000Z", :dlr=>0.029877500317357083}, + {:date_time=>"2018-11-06T00:00:00.000Z", :dlr=>0.010755975020512992}, + {:date_time=>"2018-11-05T00:00:00.000Z", :dlr=>-0.028799017690867113}, + {:date_time=>"2018-11-02T00:00:00.000Z", :dlr=>-0.0686329326726632}, + {:date_time=>"2018-11-01T00:00:00.000Z", :dlr=>0.015235626165565584}, + {:date_time=>"2018-10-31T00:00:00.000Z", :dlr=>0.025732630512992103}, + {:date_time=>"2018-10-30T00:00:00.000Z", :dlr=>0.004981915647820579}, + {:date_time=>"2018-10-29T00:00:00.000Z", :dlr=>-0.018948623129528868}, + {:date_time=>"2018-10-26T00:00:00.000Z", :dlr=>-0.0160517090105079}, + {:date_time=>"2018-10-25T00:00:00.000Z", :dlr=>0.021661496781179467}, + {:date_time=>"2018-10-24T00:00:00.000Z", :dlr=>-0.03490373037505153}, + {:date_time=>"2018-10-23T00:00:00.000Z", :dlr=>0.009382539847836957}, + {:date_time=>"2018-10-22T00:00:00.000Z", :dlr=>0.006091481696142526}, + {:date_time=>"2018-10-19T00:00:00.000Z", :dlr=>0.01511525802908552}, + {:date_time=>"2018-10-18T00:00:00.000Z", :dlr=>-0.023651064679340767}, + {:date_time=>"2018-10-17T00:00:00.000Z", :dlr=>-0.0043307687122485835}, + {:date_time=>"2018-10-16T00:00:00.000Z", :dlr=>0.02179786426382509}, + {:date_time=>"2018-10-15T00:00:00.000Z", :dlr=>-0.021617789532325248}, + {:date_time=>"2018-10-12T00:00:00.000Z", :dlr=>0.03509614368752819}, + {:date_time=>"2018-10-11T00:00:00.000Z", :dlr=>-0.008867076040336814}, + {:date_time=>"2018-10-10T00:00:00.000Z", :dlr=>-0.047433479205543055}, + {:date_time=>"2018-10-09T00:00:00.000Z", :dlr=>0.0} + ] + + expect(normalized_output).to eq(expected_output) + end + + it "Throws exception if not enough data" do + expect {indicator.calculate([], 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('dlr') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Daily Log Return') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(price_key)) + end + + it 'Validates options' do + valid_options = { price_key: :close } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = {} + expect(indicator.min_data_size(options)).to eq(1) + end + end + end +end diff --git a/spec/technical_analysis/indicators/dpo_spec.rb b/spec/technical_analysis/indicators/dpo_spec.rb new file mode 100644 index 0000000..997c99a --- /dev/null +++ b/spec/technical_analysis/indicators/dpo_spec.rb @@ -0,0 +1,90 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "DPO" do + input_data = SpecHelper.get_test_data(:close) + indicator = TechnicalAnalysis::Dpo + + describe 'Detrended Price Oscillator' do + it 'Calculates DPO (20 day)' do + output = indicator.calculate(input_data, period: 20, price_key: :close) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :dpo=>-15.774999999999977}, + {:date_time=>"2019-01-08T00:00:00.000Z", :dpo=>-19.607999999999976}, + {:date_time=>"2019-01-07T00:00:00.000Z", :dpo=>-23.730500000000006}, + {:date_time=>"2019-01-04T00:00:00.000Z", :dpo=>-24.407999999999987}, + {:date_time=>"2019-01-03T00:00:00.000Z", :dpo=>-31.726499999999987}, + {:date_time=>"2019-01-02T00:00:00.000Z", :dpo=>-17.36949999999996}, + {:date_time=>"2018-12-31T00:00:00.000Z", :dpo=>-18.922999999999973}, + {:date_time=>"2018-12-28T00:00:00.000Z", :dpo=>-21.498999999999995}, + {:date_time=>"2018-12-27T00:00:00.000Z", :dpo=>-22.642999999999972}, + {:date_time=>"2018-12-26T00:00:00.000Z", :dpo=>-22.876499999999993}, + {:date_time=>"2018-12-24T00:00:00.000Z", :dpo=>-35.0085}, + {:date_time=>"2018-12-21T00:00:00.000Z", :dpo=>-33.053}, + {:date_time=>"2018-12-20T00:00:00.000Z", :dpo=>-29.025999999999982}, + {:date_time=>"2018-12-19T00:00:00.000Z", :dpo=>-26.41850000000005}, + {:date_time=>"2018-12-18T00:00:00.000Z", :dpo=>-22.48350000000005}, + {:date_time=>"2018-12-17T00:00:00.000Z", :dpo=>-25.746500000000054}, + {:date_time=>"2018-12-14T00:00:00.000Z", :dpo=>-26.388500000000022}, + {:date_time=>"2018-12-13T00:00:00.000Z", :dpo=>-22.884000000000043}, + {:date_time=>"2018-12-12T00:00:00.000Z", :dpo=>-26.35200000000006}, + {:date_time=>"2018-12-11T00:00:00.000Z", :dpo=>-28.722000000000037}, + {:date_time=>"2018-12-10T00:00:00.000Z", :dpo=>-29.836000000000013}, + {:date_time=>"2018-12-07T00:00:00.000Z", :dpo=>-33.321500000000015}, + {:date_time=>"2018-12-06T00:00:00.000Z", :dpo=>-29.007000000000033}, + {:date_time=>"2018-12-04T00:00:00.000Z", :dpo=>-29.3245}, + {:date_time=>"2018-12-03T00:00:00.000Z", :dpo=>-22.93399999999997}, + {:date_time=>"2018-11-30T00:00:00.000Z", :dpo=>-30.462999999999965}, + {:date_time=>"2018-11-29T00:00:00.000Z", :dpo=>-30.723499999999945}, + {:date_time=>"2018-11-28T00:00:00.000Z", :dpo=>-31.052999999999997}, + {:date_time=>"2018-11-27T00:00:00.000Z", :dpo=>-39.24899999999997}, + {:date_time=>"2018-11-26T00:00:00.000Z", :dpo=>-40.02850000000001}, + {:date_time=>"2018-11-23T00:00:00.000Z", :dpo=>-43.2405}, + {:date_time=>"2018-11-21T00:00:00.000Z", :dpo=>-39.04850000000002}, + {:date_time=>"2018-11-20T00:00:00.000Z", :dpo=>-39.16900000000004}, + {:date_time=>"2018-11-19T00:00:00.000Z", :dpo=>-31.444000000000017} + ] + + 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 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('dpo') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Detrended Price Oscillator') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period price_key)) + end + + it 'Validates options' do + valid_options = { period: 22, price_key: :close } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(6) + end + end + end +end diff --git a/spec/technical_analysis/indicators/dr_spec.rb b/spec/technical_analysis/indicators/dr_spec.rb new file mode 100644 index 0000000..c360f53 --- /dev/null +++ b/spec/technical_analysis/indicators/dr_spec.rb @@ -0,0 +1,119 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "DR" do + input_data = SpecHelper.get_test_data(:close) + indicator = TechnicalAnalysis::Dr + + describe 'Daily Return' do + it 'Calculates Daily Return' do + output = indicator.calculate(input_data, price_key: :close) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :dr=>0.01698175787728018}, + {:date_time=>"2019-01-08T00:00:00.000Z", :dr=>0.019063070371121427}, + {:date_time=>"2019-01-07T00:00:00.000Z", :dr=>-0.0022258195062726527}, + {:date_time=>"2019-01-04T00:00:00.000Z", :dr=>0.042689359307968244}, + {:date_time=>"2019-01-03T00:00:00.000Z", :dr=>-0.09960739614994929}, + {:date_time=>"2019-01-02T00:00:00.000Z", :dr=>0.0011411182959297772}, + {:date_time=>"2018-12-31T00:00:00.000Z", :dr=>0.009665237150355388}, + {:date_time=>"2018-12-28T00:00:00.000Z", :dr=>0.00051232788984934}, + {:date_time=>"2018-12-27T00:00:00.000Z", :dr=>-0.006489788127505114}, + {:date_time=>"2018-12-26T00:00:00.000Z", :dr=>0.07042157597221266}, + {:date_time=>"2018-12-24T00:00:00.000Z", :dr=>-0.02587407947986453}, + {:date_time=>"2018-12-21T00:00:00.000Z", :dr=>-0.038895619460562525}, + {:date_time=>"2018-12-20T00:00:00.000Z", :dr=>-0.025234632357511222}, + {:date_time=>"2018-12-19T00:00:00.000Z", :dr=>-0.031191666164870235}, + {:date_time=>"2018-12-18T00:00:00.000Z", :dr=>0.01299255825301926}, + {:date_time=>"2018-12-17T00:00:00.000Z", :dr=>-0.009306260575296044}, + {:date_time=>"2018-12-14T00:00:00.000Z", :dr=>-0.031997660134542305}, + {:date_time=>"2018-12-13T00:00:00.000Z", :dr=>0.010940272028385545}, + {:date_time=>"2018-12-12T00:00:00.000Z", :dr=>0.0027871671707289103}, + {:date_time=>"2018-12-11T00:00:00.000Z", :dr=>-0.005719339622641484}, + {:date_time=>"2018-12-10T00:00:00.000Z", :dr=>0.006587928066947413}, + {:date_time=>"2018-12-07T00:00:00.000Z", :dr=>-0.03565705128205121}, + {:date_time=>"2018-12-06T00:00:00.000Z", :dr=>-0.011149470824608043}, + {:date_time=>"2018-12-04T00:00:00.000Z", :dr=>-0.043988745806730845}, + {:date_time=>"2018-12-03T00:00:00.000Z", :dr=>0.03494232276850706}, + {:date_time=>"2018-11-30T00:00:00.000Z", :dr=>-0.005402394876079075}, + {:date_time=>"2018-11-29T00:00:00.000Z", :dr=>-0.0076821045650491415}, + {:date_time=>"2018-11-28T00:00:00.000Z", :dr=>0.038452708907254385}, + {:date_time=>"2018-11-27T00:00:00.000Z", :dr=>-0.0021761539342571856}, + {:date_time=>"2018-11-26T00:00:00.000Z", :dr=>0.013523710023797264}, + {:date_time=>"2018-11-23T00:00:00.000Z", :dr=>-0.025398800769317886}, + {:date_time=>"2018-11-21T00:00:00.000Z", :dr=>-0.0011300711944851605}, + {:date_time=>"2018-11-20T00:00:00.000Z", :dr=>-0.047777897342085596}, + {:date_time=>"2018-11-19T00:00:00.000Z", :dr=>-0.03963209838267967}, + {:date_time=>"2018-11-16T00:00:00.000Z", :dr=>0.011075701374013924}, + {:date_time=>"2018-11-15T00:00:00.000Z", :dr=>0.02467880085653107}, + {:date_time=>"2018-11-14T00:00:00.000Z", :dr=>-0.02824741195442948}, + {:date_time=>"2018-11-13T00:00:00.000Z", :dr=>-0.009991244785497289}, + {:date_time=>"2018-11-12T00:00:00.000Z", :dr=>-0.05037413801535684}, + {:date_time=>"2018-11-09T00:00:00.000Z", :dr=>-0.019281500311765565}, + {:date_time=>"2018-11-08T00:00:00.000Z", :dr=>-0.006954036675398845}, + {:date_time=>"2018-11-07T00:00:00.000Z", :dr=>0.030328311331403013}, + {:date_time=>"2018-11-06T00:00:00.000Z", :dr=>0.010814028473634663}, + {:date_time=>"2018-11-05T00:00:00.000Z", :dr=>-0.028388278388278287}, + {:date_time=>"2018-11-02T00:00:00.000Z", :dr=>-0.06633066330663306}, + {:date_time=>"2018-11-01T00:00:00.000Z", :dr=>0.015352279996344587}, + {:date_time=>"2018-10-31T00:00:00.000Z", :dr=>0.026066572902015972}, + {:date_time=>"2018-10-30T00:00:00.000Z", :dr=>0.004994346023369678}, + {:date_time=>"2018-10-29T00:00:00.000Z", :dr=>-0.018770226537216828}, + {:date_time=>"2018-10-26T00:00:00.000Z", :dr=>-0.015923566878980888}, + {:date_time=>"2018-10-25T00:00:00.000Z", :dr=>0.021897810218978186}, + {:date_time=>"2018-10-24T00:00:00.000Z", :dr=>-0.034301620796480026}, + {:date_time=>"2018-10-23T00:00:00.000Z", :dr=>0.009426693859052815}, + {:date_time=>"2018-10-22T00:00:00.000Z", :dr=>0.006110072500113972}, + {:date_time=>"2018-10-19T00:00:00.000Z", :dr=>0.015230071289695335}, + {:date_time=>"2018-10-18T00:00:00.000Z", :dr=>-0.023373570233735652}, + {:date_time=>"2018-10-17T00:00:00.000Z", :dr=>-0.004321404456448352}, + {:date_time=>"2018-10-16T00:00:00.000Z", :dr=>0.022037173352962736}, + {:date_time=>"2018-10-15T00:00:00.000Z", :dr=>-0.021385799828913643}, + {:date_time=>"2018-10-12T00:00:00.000Z", :dr=>0.035719281883889176}, + {:date_time=>"2018-10-11T00:00:00.000Z", :dr=>-0.00882787946015906}, + {:date_time=>"2018-10-10T00:00:00.000Z", :dr=>-0.046326089831180806}, + {:date_time=>"2018-10-09T00:00:00.000Z", :dr=>0.0} + ] + + expect(normalized_output).to eq(expected_output) + end + + it "Throws exception if not enough data" do + expect {indicator.calculate([], 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('dr') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Daily Return') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(price_key)) + end + + it 'Validates options' do + valid_options = { price_key: :close } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = {} + expect(indicator.min_data_size(options)).to eq(1) + end + end + end +end diff --git a/spec/technical_analysis/indicators/ema_spec.rb b/spec/technical_analysis/indicators/ema_spec.rb new file mode 100644 index 0000000..15da40a --- /dev/null +++ b/spec/technical_analysis/indicators/ema_spec.rb @@ -0,0 +1,115 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "EMA" do + input_data = SpecHelper.get_test_data(:close, date_time_key: :timestep) + indicator = TechnicalAnalysis::Ema + + describe 'Exponential Moving Average' do + it 'Calculates EMA (5 day)' do + output = indicator.calculate(input_data, period: 5, price_key: :close, date_time_key: :timestep) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :ema=>151.1937160522574}, + {:date_time=>"2019-01-08T00:00:00.000Z", :ema=>150.1355740783861}, + {:date_time=>"2019-01-07T00:00:00.000Z", :ema=>149.82836111757913}, + {:date_time=>"2019-01-04T00:00:00.000Z", :ema=>150.77754167636868}, + {:date_time=>"2019-01-03T00:00:00.000Z", :ema=>152.03631251455303}, + {:date_time=>"2019-01-02T00:00:00.000Z", :ema=>156.95946877182956}, + {:date_time=>"2018-12-31T00:00:00.000Z", :ema=>156.47920315774434}, + {:date_time=>"2018-12-28T00:00:00.000Z", :ema=>155.8488047366165}, + {:date_time=>"2018-12-27T00:00:00.000Z", :ema=>155.65820710492477}, + {:date_time=>"2018-12-26T00:00:00.000Z", :ema=>155.41231065738714}, + {:date_time=>"2018-12-24T00:00:00.000Z", :ema=>154.53346598608073}, + {:date_time=>"2018-12-21T00:00:00.000Z", :ema=>158.3851989791211}, + {:date_time=>"2018-12-20T00:00:00.000Z", :ema=>162.21279846868165}, + {:date_time=>"2018-12-19T00:00:00.000Z", :ema=>164.90419770302248}, + {:date_time=>"2018-12-18T00:00:00.000Z", :ema=>166.91129655453372}, + {:date_time=>"2018-12-17T00:00:00.000Z", :ema=>167.3319448318006}, + {:date_time=>"2018-12-14T00:00:00.000Z", :ema=>169.02791724770088}, + {:date_time=>"2018-12-13T00:00:00.000Z", :ema=>170.80187587155132}, + {:date_time=>"2018-12-12T00:00:00.000Z", :ema=>170.72781380732698}, + {:date_time=>"2018-12-11T00:00:00.000Z", :ema=>171.54172071099046}, + {:date_time=>"2018-12-10T00:00:00.000Z", :ema=>172.99758106648568}, + {:date_time=>"2018-12-07T00:00:00.000Z", :ema=>174.69637159972854}, + {:date_time=>"2018-12-06T00:00:00.000Z", :ema=>177.7995573995928}, + {:date_time=>"2018-12-04T00:00:00.000Z", :ema=>179.33933609938921}, + {:date_time=>"2018-12-03T00:00:00.000Z", :ema=>180.6640041490838}, + {:date_time=>"2018-11-30T00:00:00.000Z", :ema=>178.58600622362573}, + {:date_time=>"2018-11-29T00:00:00.000Z", :ema=>178.5890093354386}, + {:date_time=>"2018-11-28T00:00:00.000Z", :ema=>178.10851400315786}, + {:date_time=>"2018-11-27T00:00:00.000Z", :ema=>176.69277100473678}, + {:date_time=>"2018-11-26T00:00:00.000Z", :ema=>177.91915650710516}, + {:date_time=>"2018-11-23T00:00:00.000Z", :ema=>179.56873476065772}, + {:date_time=>"2018-11-21T00:00:00.000Z", :ema=>183.20810214098657}, + {:date_time=>"2018-11-20T00:00:00.000Z", :ema=>186.42215321147984}, + {:date_time=>"2018-11-19T00:00:00.000Z", :ema=>191.14322981721975}, + {:date_time=>"2018-11-16T00:00:00.000Z", :ema=>193.7848447258296}, + {:date_time=>"2018-11-15T00:00:00.000Z", :ema=>193.9122670887444}, + {:date_time=>"2018-11-14T00:00:00.000Z", :ema=>195.1634006331166}, + {:date_time=>"2018-11-13T00:00:00.000Z", :ema=>199.34510094967487}, + {:date_time=>"2018-11-12T00:00:00.000Z", :ema=>202.90265142451233}, + {:date_time=>"2018-11-09T00:00:00.000Z", :ema=>207.26897713676848}, + {:date_time=>"2018-11-08T00:00:00.000Z", :ema=>208.66846570515273}, + {:date_time=>"2018-11-07T00:00:00.000Z", :ema=>208.75769855772907}, + {:date_time=>"2018-11-06T00:00:00.000Z", :ema=>208.16154783659363}, + {:date_time=>"2018-11-05T00:00:00.000Z", :ema=>210.35732175489045}, + {:date_time=>"2018-11-02T00:00:00.000Z", :ema=>214.7409826323357}, + {:date_time=>"2018-11-01T00:00:00.000Z", :ema=>218.37147394850354}, + {:date_time=>"2018-10-31T00:00:00.000Z", :ema=>216.44721092275532}, + {:date_time=>"2018-10-30T00:00:00.000Z", :ema=>215.24081638413296}, + {:date_time=>"2018-10-29T00:00:00.000Z", :ema=>216.21122457619944}, + {:date_time=>"2018-10-26T00:00:00.000Z", :ema=>218.19683686429914}, + {:date_time=>"2018-10-25T00:00:00.000Z", :ema=>219.1452552964487}, + {:date_time=>"2018-10-24T00:00:00.000Z", :ema=>218.81788294467307}, + {:date_time=>"2018-10-23T00:00:00.000Z", :ema=>220.6818244170096}, + {:date_time=>"2018-10-22T00:00:00.000Z", :ema=>219.65773662551442}, + {:date_time=>"2018-10-19T00:00:00.000Z", :ema=>219.1616049382716}, + {:date_time=>"2018-10-18T00:00:00.000Z", :ema=>219.0874074074074}, + {:date_time=>"2018-10-17T00:00:00.000Z", :ema=>220.6211111111111}, + {:date_time=>"2018-10-16T00:00:00.000Z", :ema=>220.33666666666667}, + {:date_time=>"2018-10-15T00:00:00.000Z", :ema=>219.43} + ] + + 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 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('ema') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Exponential Moving Average') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period price_key date_time_key)) + end + + it 'Validates options' do + valid_options = { period: 22, price_key: :close, date_time_key: :timestep } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(4) + end + end + end +end diff --git a/spec/technical_analysis/indicators/eom_spec.rb b/spec/technical_analysis/indicators/eom_spec.rb new file mode 100644 index 0000000..9fb20c1 --- /dev/null +++ b/spec/technical_analysis/indicators/eom_spec.rb @@ -0,0 +1,105 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "EoM" do + input_data = SpecHelper.get_test_data(:high, :low, :volume) + indicator = TechnicalAnalysis::Eom + + describe 'Ease of Movement' do + it 'Calculates EoM (14 day)' do + output = indicator.calculate(input_data, period: 14) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :eom=>-6.497226050937367}, + {:date_time=>"2019-01-08T00:00:00.000Z", :eom=>-7.7025655861800235}, + {:date_time=>"2019-01-07T00:00:00.000Z", :eom=>-10.852331038262774}, + {:date_time=>"2019-01-04T00:00:00.000Z", :eom=>-13.901372232239117}, + {:date_time=>"2019-01-03T00:00:00.000Z", :eom=>-14.868322149693872}, + {:date_time=>"2019-01-02T00:00:00.000Z", :eom=>-10.542472341445862}, + {:date_time=>"2018-12-31T00:00:00.000Z", :eom=>-7.266128029998595}, + {:date_time=>"2018-12-28T00:00:00.000Z", :eom=>-11.754905920393293}, + {:date_time=>"2018-12-27T00:00:00.000Z", :eom=>-15.149439471732872}, + {:date_time=>"2018-12-26T00:00:00.000Z", :eom=>-21.397258702024285}, + {:date_time=>"2018-12-24T00:00:00.000Z", :eom=>-29.127850557035778}, + {:date_time=>"2018-12-21T00:00:00.000Z", :eom=>-21.640253827328284}, + {:date_time=>"2018-12-20T00:00:00.000Z", :eom=>-19.510117531121228}, + {:date_time=>"2018-12-19T00:00:00.000Z", :eom=>-14.184550665717635}, + {:date_time=>"2018-12-18T00:00:00.000Z", :eom=>-5.583810139052782}, + {:date_time=>"2018-12-17T00:00:00.000Z", :eom=>-5.714363954096145}, + {:date_time=>"2018-12-14T00:00:00.000Z", :eom=>-5.49920090817078}, + {:date_time=>"2018-12-13T00:00:00.000Z", :eom=>-8.427863395925653}, + {:date_time=>"2018-12-12T00:00:00.000Z", :eom=>-8.897024349696329}, + {:date_time=>"2018-12-11T00:00:00.000Z", :eom=>-15.409243349973261}, + {:date_time=>"2018-12-10T00:00:00.000Z", :eom=>-21.68916026908629}, + {:date_time=>"2018-12-07T00:00:00.000Z", :eom=>-15.003602992747133}, + {:date_time=>"2018-12-06T00:00:00.000Z", :eom=>-14.327931101318542}, + {:date_time=>"2018-12-04T00:00:00.000Z", :eom=>-13.565693513893978}, + {:date_time=>"2018-12-03T00:00:00.000Z", :eom=>-11.780616477806618}, + {:date_time=>"2018-11-30T00:00:00.000Z", :eom=>-20.874548142667777}, + {:date_time=>"2018-11-29T00:00:00.000Z", :eom=>-23.304959556064855}, + {:date_time=>"2018-11-28T00:00:00.000Z", :eom=>-23.906907831421474}, + {:date_time=>"2018-11-27T00:00:00.000Z", :eom=>-24.18360420308819}, + {:date_time=>"2018-11-26T00:00:00.000Z", :eom=>-23.02094878061384}, + {:date_time=>"2018-11-23T00:00:00.000Z", :eom=>-27.26817615617346}, + {:date_time=>"2018-11-21T00:00:00.000Z", :eom=>-28.2247053700715}, + {:date_time=>"2018-11-20T00:00:00.000Z", :eom=>-27.37028760395389}, + {:date_time=>"2018-11-19T00:00:00.000Z", :eom=>-16.94506299946587}, + {:date_time=>"2018-11-16T00:00:00.000Z", :eom=>-13.43297156412281}, + {:date_time=>"2018-11-15T00:00:00.000Z", :eom=>-23.978202090280167}, + {:date_time=>"2018-11-14T00:00:00.000Z", :eom=>-26.374761082588922}, + {:date_time=>"2018-11-13T00:00:00.000Z", :eom=>-22.593717509723117}, + {:date_time=>"2018-11-12T00:00:00.000Z", :eom=>-19.69000228424736}, + {:date_time=>"2018-11-09T00:00:00.000Z", :eom=>-16.918604972309158}, + {:date_time=>"2018-11-08T00:00:00.000Z", :eom=>-11.567473480653877}, + {:date_time=>"2018-11-07T00:00:00.000Z", :eom=>-10.367227984759097}, + {:date_time=>"2018-11-06T00:00:00.000Z", :eom=>-22.183583990006944}, + {:date_time=>"2018-11-05T00:00:00.000Z", :eom=>-22.338003197707696}, + {:date_time=>"2018-11-02T00:00:00.000Z", :eom=>-16.2797807192085}, + {:date_time=>"2018-11-01T00:00:00.000Z", :eom=>-10.135395436615527}, + {:date_time=>"2018-10-31T00:00:00.000Z", :eom=>-6.606596973217312}, + {:date_time=>"2018-10-30T00:00:00.000Z", :eom=>-16.275660385464448}, + {:date_time=>"2018-10-29T00:00:00.000Z", :eom=>-21.877975231994814} + ] + + 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+2)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('eom') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Ease of Movement') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period)) + end + + it 'Validates options' do + valid_options = { period: 22 } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(5) + end + end + end +end diff --git a/spec/technical_analysis/indicators/fi_spec.rb b/spec/technical_analysis/indicators/fi_spec.rb new file mode 100644 index 0000000..850a5e0 --- /dev/null +++ b/spec/technical_analysis/indicators/fi_spec.rb @@ -0,0 +1,118 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "FI" do + input_data = SpecHelper.get_test_data(:close, :volume) + indicator = TechnicalAnalysis::Fi + + describe 'Forced Index' do + it 'Calculates FI' do + output = indicator.calculate(input_data) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :fi=>115287987.2000001}, + {:date_time=>"2019-01-08T00:00:00.000Z", :fi=>114556606.19999972}, + {:date_time=>"2019-01-07T00:00:00.000Z", :fi=>-18008575.19999913}, + {:date_time=>"2019-01-04T00:00:00.000Z", :fi=>348561555.4999996}, + {:date_time=>"2019-01-03T00:00:00.000Z", :fi=>-1433110593.199999}, + {:date_time=>"2019-01-02T00:00:00.000Z", :fi=>6414672.59999923}, + {:date_time=>"2018-12-31T00:00:00.000Z", :fi=>52094078.90000067}, + {:date_time=>"2018-12-28T00:00:00.000Z", :fi=>3339247.9999993355}, + {:date_time=>"2018-12-27T00:00:00.000Z", :fi=>-52641026.99999906}, + {:date_time=>"2018-12-26T00:00:00.000Z", :fi=>601104008.9999986}, + {:date_time=>"2018-12-24T00:00:00.000Z", :fi=>-144959996.99999917}, + {:date_time=>"2018-12-21T00:00:00.000Z", :fi=>-582537190.0000021}, + {:date_time=>"2018-12-20T00:00:00.000Z", :fi=>-261456813.7999983}, + {:date_time=>"2018-12-19T00:00:00.000Z", :fi=>-246555930.60000032}, + {:date_time=>"2018-12-18T00:00:00.000Z", :fi=>71894933.69999984}, + {:date_time=>"2018-12-17T00:00:00.000Z", :fi=>-66605646.799999654}, + {:date_time=>"2018-12-14T00:00:00.000Z", :fi=>-222193369.19999996}, + {:date_time=>"2018-12-13T00:00:00.000Z", :fi=>58745288.49999982}, + {:date_time=>"2018-12-12T00:00:00.000Z", :fi=>16673099.59999996}, + {:date_time=>"2018-12-11T00:00:00.000Z", :fi=>-44588998.799999945}, + {:date_time=>"2018-12-10T00:00:00.000Z", :fi=>68552489.99999909}, + {:date_time=>"2018-12-07T00:00:00.000Z", :fi=>-259658176.39999956}, + {:date_time=>"2018-12-06T00:00:00.000Z", :fi=>-84128672.69999996}, + {:date_time=>"2018-12-04T00:00:00.000Z", :fi=>-334478362.4999998}, + {:date_time=>"2018-12-03T00:00:00.000Z", :fi=>252955247.99999923}, + {:date_time=>"2018-11-30T00:00:00.000Z", :fi=>-38241532.19999996}, + {:date_time=>"2018-11-29T00:00:00.000Z", :fi=>-57717776.19999944}, + {:date_time=>"2018-11-28T00:00:00.000Z", :fi=>307809724.99999946}, + {:date_time=>"2018-11-27T00:00:00.000Z", :fi=>-15639333.199999813}, + {:date_time=>"2018-11-26T00:00:00.000Z", :fi=>104063205.60000056}, + {:date_time=>"2018-11-23T00:00:00.000Z", :fi=>-106071625.30000022}, + {:date_time=>"2018-11-21T00:00:00.000Z", :fi=>-6219247.999999646}, + {:date_time=>"2018-11-20T00:00:00.000Z", :fi=>-600986678.4000016}, + {:date_time=>"2018-11-19T00:00:00.000Z", :fi=>-319277709.3999995}, + {:date_time=>"2018-11-16T00:00:00.000Z", :fi=>76725619.60000016}, + {:date_time=>"2018-11-15T00:00:00.000Z", :fi=>213312352.5999993}, + {:date_time=>"2018-11-14T00:00:00.000Z", :fi=>-328772056.1999987}, + {:date_time=>"2018-11-13T00:00:00.000Z", :fi=>-90647877.39999989}, + {:date_time=>"2018-11-12T00:00:00.000Z", :fi=>-525207609.0000006}, + {:date_time=>"2018-11-09T00:00:00.000Z", :fi=>-137957395.20000035}, + {:date_time=>"2018-11-08T00:00:00.000Z", :fi=>-36922334.19999948}, + {:date_time=>"2018-11-07T00:00:00.000Z", :fi=>205742335.19999927}, + {:date_time=>"2018-11-06T00:00:00.000Z", :fi=>69268889.60000022}, + {:date_time=>"2018-11-05T00:00:00.000Z", :fi=>-389165081.2999991}, + {:date_time=>"2018-11-02T00:00:00.000Z", :fi=>-1342026294.4000008}, + {:date_time=>"2018-11-01T00:00:00.000Z", :fi=>177925675.1999992}, + {:date_time=>"2018-10-31T00:00:00.000Z", :fi=>211373463.60000008}, + {:date_time=>"2018-10-30T00:00:00.000Z", :fi=>38677205.80000009}, + {:date_time=>"2018-10-29T00:00:00.000Z", :fi=>-185597581.4000001}, + {:date_time=>"2018-10-26T00:00:00.000Z", :fi=>-165170950.0}, + {:date_time=>"2018-10-25T00:00:00.000Z", :fi=>136718771.40000024}, + {:date_time=>"2018-10-24T00:00:00.000Z", :fi=>-305539796.7999995}, + {:date_time=>"2018-10-23T00:00:00.000Z", :fi=>80456833.59999938}, + {:date_time=>"2018-10-22T00:00:00.000Z", :fi=>38527063.6000001}, + {:date_time=>"2018-10-19T00:00:00.000Z", :fi=>108156545.69999973}, + {:date_time=>"2018-10-18T00:00:00.000Z", :fi=>-167452577.5999996}, + {:date_time=>"2018-10-17T00:00:00.000Z", :fi=>-21785164.80000018}, + {:date_time=>"2018-10-16T00:00:00.000Z", :fi=>137964214.49999976}, + {:date_time=>"2018-10-15T00:00:00.000Z", :fi=>-143832137.5}, + {:date_time=>"2018-10-12T00:00:00.000Z", :fi=>302529938.200001}, + {:date_time=>"2018-10-11T00:00:00.000Z", :fi=>-101043431.20000133}, + {:date_time=>"2018-10-10T00:00:00.000Z", :fi=>-431793575.69999963} + ] + + expect(normalized_output).to eq(expected_output) + end + + it "Throws exception if not enough data" do + expect {indicator.calculate([])}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('fi') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Force Index') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq([]) + end + + it 'Validates options' do + valid_options = {} + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = {} + expect(indicator.min_data_size(options)).to eq(2) + end + end + end +end diff --git a/spec/technical_analysis/indicators/ichimoku_spec.rb b/spec/technical_analysis/indicators/ichimoku_spec.rb new file mode 100644 index 0000000..7101a42 --- /dev/null +++ b/spec/technical_analysis/indicators/ichimoku_spec.rb @@ -0,0 +1,95 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "Ichimoku" do + input_data = SpecHelper.get_test_data(:high, :low, :close) + indicator = TechnicalAnalysis::Ichimoku + + describe 'Ichimoku' do + it 'Calculates Ichimoku' do + output = indicator.calculate(input_data, low_period: 3, medium_period: 10, high_period: 20) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :chikou_span=>157.17, :kijun_sen=>150.68, :senkou_span_a=>155.9775, :senkou_span_b=>165.765, :tenkan_sen=>150.215}, + {:date_time=>"2019-01-08T00:00:00.000Z", :chikou_span=>146.83, :kijun_sen=>150.68, :senkou_span_a=>156.965, :senkou_span_b=>165.765, :tenkan_sen=>147.81}, + {:date_time=>"2019-01-07T00:00:00.000Z", :chikou_span=>150.73, :kijun_sen=>150.68, :senkou_span_a=>159.82, :senkou_span_b=>167.285, :tenkan_sen=>145.41500000000002}, + {:date_time=>"2019-01-04T00:00:00.000Z", :chikou_span=>156.83, :kijun_sen=>152.055, :senkou_span_a=>163.15500000000003, :senkou_span_b=>170.12, :tenkan_sen=>150.425}, + {:date_time=>"2019-01-03T00:00:00.000Z", :chikou_span=>160.89, :kijun_sen=>154.725, :senkou_span_a=>165.3275, :senkou_span_b=>172.015, :tenkan_sen=>150.68}, + {:date_time=>"2019-01-02T00:00:00.000Z", :chikou_span=>166.07, :kijun_sen=>157.06, :senkou_span_a=>169.23247500000002, :senkou_span_b=>176.71499999999997, :tenkan_sen=>156.79500000000002}, + {:date_time=>"2018-12-31T00:00:00.000Z", :chikou_span=>163.94, :kijun_sen=>157.47, :senkou_span_a=>170.74249999999998, :senkou_span_b=>178.84975, :tenkan_sen=>154.715}, + {:date_time=>"2018-12-28T00:00:00.000Z", :chikou_span=>165.48, :kijun_sen=>157.835, :senkou_span_a=>171.53, :senkou_span_b=>179.14975, :tenkan_sen=>152.62}, + {:date_time=>"2018-12-27T00:00:00.000Z", :chikou_span=>170.95, :kijun_sen=>159.57999999999998, :senkou_span_a=>171.95999999999998, :senkou_span_b=>179.14975, :tenkan_sen=>151.91}, + {:date_time=>"2018-12-26T00:00:00.000Z", :chikou_span=>169.1, :kijun_sen=>159.57999999999998, :senkou_span_a=>170.88, :senkou_span_b=>180.255, :tenkan_sen=>152.375}, + {:date_time=>"2018-12-24T00:00:00.000Z", :chikou_span=>168.63, :kijun_sen=>159.57999999999998, :senkou_span_a=>171.5225, :senkou_span_b=>181.59, :tenkan_sen=>154.35000000000002}, + {:date_time=>"2018-12-21T00:00:00.000Z", :chikou_span=>169.6, :kijun_sen=>161.1, :senkou_span_a=>171.595, :senkou_span_b=>184.67000000000002, :tenkan_sen=>158.54}, + {:date_time=>"2018-12-20T00:00:00.000Z", :chikou_span=>168.49, :kijun_sen=>164.895, :senkou_span_a=>175.98247500000002, :senkou_span_b=>189.21, :tenkan_sen=>161.41500000000002}, + {:date_time=>"2018-12-19T00:00:00.000Z", :chikou_span=>174.72, :kijun_sen=>166.935, :senkou_span_a=>177.64, :senkou_span_b=>190.19, :tenkan_sen=>163.72}, + {:date_time=>"2018-12-18T00:00:00.000Z", :chikou_span=>176.69, :kijun_sen=>172.55995000000001, :senkou_span_a=>179.10250000000002, :senkou_span_b=>190.19, :tenkan_sen=>165.905}, + {:date_time=>"2018-12-17T00:00:00.000Z", :chikou_span=>184.82, :kijun_sen=>173.83499999999998, :senkou_span_a=>180.73250000000002, :senkou_span_b=>190.19, :tenkan_sen=>167.64999999999998}, + {:date_time=>"2018-12-14T00:00:00.000Z", :chikou_span=>178.58, :kijun_sen=>174.135, :senkou_span_a=>180.739875, :senkou_span_b=>191.95499999999998, :tenkan_sen=>168.925}, + {:date_time=>"2018-12-13T00:00:00.000Z", :chikou_span=>179.55, :kijun_sen=>174.135, :senkou_span_a=>179.727375, :senkou_span_b=>196.31, :tenkan_sen=>169.785}, + {:date_time=>"2018-12-12T00:00:00.000Z", :chikou_span=>180.94, :kijun_sen=>174.135, :senkou_span_a=>179.194875, :senkou_span_b=>196.31, :tenkan_sen=>167.625}, + {:date_time=>"2018-12-11T00:00:00.000Z", :chikou_span=>174.24, :kijun_sen=>174.135, :senkou_span_a=>178.57375000000002, :senkou_span_b=>196.31, :tenkan_sen=>168.91000000000003}, + {:date_time=>"2018-12-10T00:00:00.000Z", :chikou_span=>174.62, :kijun_sen=>174.135, :senkou_span_a=>180.16, :senkou_span_b=>196.31, :tenkan_sen=>169.055}, + {:date_time=>"2018-12-07T00:00:00.000Z", :chikou_span=>172.29, :kijun_sen=>176.62, :senkou_span_a=>182.92000000000002, :senkou_span_b=>197.23000000000002, :tenkan_sen=>175.34495}, + {:date_time=>"2018-12-06T00:00:00.000Z", :chikou_span=>176.78, :kijun_sen=>177.6, :senkou_span_a=>187.95999999999998, :senkou_span_b=>198.935, :tenkan_sen=>177.68}, + {:date_time=>"2018-12-04T00:00:00.000Z", :chikou_span=>176.98, :kijun_sen=>177.6, :senkou_span_a=>189.027375, :senkou_span_b=>199.87, :tenkan_sen=>180.60500000000002}, + {:date_time=>"2018-12-03T00:00:00.000Z", :chikou_span=>185.86, :kijun_sen=>180.48, :senkou_span_a=>193.76737500000002, :senkou_span_b=>204.61, :tenkan_sen=>180.985}, + {:date_time=>"2018-11-30T00:00:00.000Z", :chikou_span=>193.53, :kijun_sen=>182.61475000000002, :senkou_span_a=>194.237375, :senkou_span_b=>205.07999999999998, :tenkan_sen=>178.865}, + {:date_time=>"2018-11-29T00:00:00.000Z", :chikou_span=>191.41, :kijun_sen=>182.61475000000002, :senkou_span_a=>195.6725, :senkou_span_b=>205.07999999999998, :tenkan_sen=>176.84}, + {:date_time=>"2018-11-28T00:00:00.000Z", :chikou_span=>186.8, :kijun_sen=>182.61475000000002, :senkou_span_a=>198.51749999999998, :senkou_span_b=>205.07999999999998, :tenkan_sen=>175.77499999999998}, + {:date_time=>"2018-11-27T00:00:00.000Z", :chikou_span=>192.23, :kijun_sen=>183.72, :senkou_span_a=>202.81755, :senkou_span_b=>207.84005, :tenkan_sen=>173.4275}, + {:date_time=>"2018-11-26T00:00:00.000Z", :chikou_span=>194.17, :kijun_sen=>185.055, :senkou_span_a=>205.015, :senkou_span_b=>209.01, :tenkan_sen=>175.265}, + {:date_time=>"2018-11-23T00:00:00.000Z", :chikou_span=>204.47, :kijun_sen=>189.055, :senkou_span_a=>208.225, :senkou_span_b=>211.2, :tenkan_sen=>176.785}, + {:date_time=>"2018-11-21T00:00:00.000Z", :chikou_span=>208.49, :kijun_sen=>192.815, :senkou_span_a=>208.08499999999998, :senkou_span_b=>211.2, :tenkan_sen=>183.105}, + {:date_time=>"2018-11-20T00:00:00.000Z", :chikou_span=>209.95, :kijun_sen=>192.815, :senkou_span_a=>207.19, :senkou_span_b=>211.2, :tenkan_sen=>185.23975000000002}, + {:date_time=>"2018-11-19T00:00:00.000Z", :chikou_span=>203.77, :kijun_sen=>197.555, :senkou_span_a=>208.555, :senkou_span_b=>212.26, :tenkan_sen=>189.97975000000002}, + {:date_time=>"2018-11-16T00:00:00.000Z", :chikou_span=>201.59, :kijun_sen=>198.025, :senkou_span_a=>210.7325, :senkou_span_b=>212.72, :tenkan_sen=>190.44975} + ] + + expect(normalized_output).to eq(expected_output) + end + + it "Throws exception if not enough data" do + medium_period = 20 + high_period = 40 + size_limit = (medium_period + high_period + 1) + + expect {indicator.calculate(input_data, high_period: size_limit)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('ichimoku') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Ichimoku Kinko Hyo') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(low_period medium_period high_period)) + end + + it 'Validates options' do + valid_options = { low_period: 9, medium_period: 26, high_period: 52 } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { medium_period: 4, high_period: 10 } + expect(indicator.min_data_size(options)).to eq(13) + end + end + end +end diff --git a/spec/technical_analysis/indicators/indicator_spec.rb b/spec/technical_analysis/indicators/indicator_spec.rb new file mode 100644 index 0000000..f8ed04d --- /dev/null +++ b/spec/technical_analysis/indicators/indicator_spec.rb @@ -0,0 +1,120 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "Indicator" do + indicator = TechnicalAnalysis::Indicator + + it 'Returns nil on a nonexistant indicator' do + nonexistant_indicator = indicator.find('test') + expect(nonexistant_indicator).to eq(nil) + end + + it 'Finds an indicator' do + sma = indicator.find('sma') + expect(sma).to eq(TechnicalAnalysis::Sma) + end + + describe 'Calculations' do + it 'Returns nil on a nonexistant calculation' do + calculation = indicator.calculate('sma', [], :test, { period: 20, price_key: :close }) + expect(calculation).to eq(nil) + end + + it 'Calculates indicator_name' do + indicator_name = indicator.calculate('sma', [], :indicator_name, { period: 20, price_key: :close }) + expect(indicator_name).to eq('Simple Moving Average') + end + + it 'Calculates indicator_symbol' do + indicator_name = indicator.calculate('sma', [], :indicator_symbol, { period: 20, price_key: :close }) + expect(indicator_name).to eq('sma') + end + + it 'Calculates min_data_size' do + min_data_size = indicator.calculate('sma', [], :min_data_size, { period: 20, price_key: :close }) + expect(min_data_size).to eq(20) + end + + it 'Calculates valid_options' do + valid_options = indicator.calculate('sma', [], :valid_options, { period: 20, price_key: :close }) + expect(valid_options).to eq(%i(period price_key date_time_key)) + end + + it 'Calculates validate_options' do + options_validated = indicator.calculate('sma', [], :validate_options, { period: 20, price_key: :close }) + expect(options_validated).to eq(true) + end + + it 'Calculates technicals' do + input_data = SpecHelper.get_test_data(:close) + output = indicator.calculate('sma', input_data, :technicals, { period: 5, price_key: :close }) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :sma=>148.488}, + {:date_time=>"2019-01-08T00:00:00.000Z", :sma=>149.41}, + {:date_time=>"2019-01-07T00:00:00.000Z", :sma=>150.808}, + {:date_time=>"2019-01-04T00:00:00.000Z", :sma=>152.468}, + {:date_time=>"2019-01-03T00:00:00.000Z", :sma=>154.046}, + {:date_time=>"2019-01-02T00:00:00.000Z", :sma=>157.04199999999997}, + {:date_time=>"2018-12-31T00:00:00.000Z", :sma=>154.824}, + {:date_time=>"2018-12-28T00:00:00.000Z", :sma=>153.422}, + {:date_time=>"2018-12-27T00:00:00.000Z", :sma=>153.54199999999997}, + {:date_time=>"2018-12-26T00:00:00.000Z", :sma=>154.49}, + {:date_time=>"2018-12-24T00:00:00.000Z", :sma=>156.27}, + {:date_time=>"2018-12-21T00:00:00.000Z", :sma=>159.692}, + {:date_time=>"2018-12-20T00:00:00.000Z", :sma=>162.642}, + {:date_time=>"2018-12-19T00:00:00.000Z", :sma=>165.46599999999998}, + {:date_time=>"2018-12-18T00:00:00.000Z", :sma=>167.108}, + {:date_time=>"2018-12-17T00:00:00.000Z", :sma=>167.61999999999998}, + {:date_time=>"2018-12-14T00:00:00.000Z", :sma=>168.752}, + {:date_time=>"2018-12-13T00:00:00.000Z", :sma=>169.35399999999998}, + {:date_time=>"2018-12-12T00:00:00.000Z", :sma=>170.108}, + {:date_time=>"2018-12-11T00:00:00.000Z", :sma=>171.626}, + {:date_time=>"2018-12-10T00:00:00.000Z", :sma=>174.864}, + {:date_time=>"2018-12-07T00:00:00.000Z", :sma=>176.66}, + {:date_time=>"2018-12-06T00:00:00.000Z", :sma=>178.872}, + {:date_time=>"2018-12-04T00:00:00.000Z", :sma=>180.11600000000004}, + {:date_time=>"2018-12-03T00:00:00.000Z", :sma=>179.62600000000003}, + {:date_time=>"2018-11-30T00:00:00.000Z", :sma=>177.58599999999998}, + {:date_time=>"2018-11-29T00:00:00.000Z", :sma=>176.32799999999997}, + {:date_time=>"2018-11-28T00:00:00.000Z", :sma=>175.77400000000003}, + {:date_time=>"2018-11-27T00:00:00.000Z", :sma=>174.982}, + {:date_time=>"2018-11-26T00:00:00.000Z", :sma=>177.30599999999998}, + {:date_time=>"2018-11-23T00:00:00.000Z", :sma=>181.088}, + {:date_time=>"2018-11-21T00:00:00.000Z", :sma=>184.91199999999998}, + {:date_time=>"2018-11-20T00:00:00.000Z", :sma=>186.916}, + {:date_time=>"2018-11-19T00:00:00.000Z", :sma=>189.96599999999998}, + {:date_time=>"2018-11-16T00:00:00.000Z", :sma=>191.628}, + {:date_time=>"2018-11-15T00:00:00.000Z", :sma=>193.816}, + {:date_time=>"2018-11-14T00:00:00.000Z", :sma=>197.23200000000003}, + {:date_time=>"2018-11-13T00:00:00.000Z", :sma=>201.862}, + {:date_time=>"2018-11-12T00:00:00.000Z", :sma=>204.17000000000002}, + {:date_time=>"2018-11-09T00:00:00.000Z", :sma=>205.654}, + {:date_time=>"2018-11-08T00:00:00.000Z", :sma=>206.256}, + {:date_time=>"2018-11-07T00:00:00.000Z", :sma=>209.002}, + {:date_time=>"2018-11-06T00:00:00.000Z", :sma=>210.78400000000002}, + {:date_time=>"2018-11-05T00:00:00.000Z", :sma=>212.69}, + {:date_time=>"2018-11-02T00:00:00.000Z", :sma=>214.82000000000002}, + {:date_time=>"2018-11-01T00:00:00.000Z", :sma=>216.584}, + {:date_time=>"2018-10-31T00:00:00.000Z", :sma=>216.1}, + {:date_time=>"2018-10-30T00:00:00.000Z", :sma=>215.346}, + {:date_time=>"2018-10-29T00:00:00.000Z", :sma=>217.23200000000003}, + {:date_time=>"2018-10-26T00:00:00.000Z", :sma=>218.914}, + {:date_time=>"2018-10-25T00:00:00.000Z", :sma=>219.51600000000002}, + {:date_time=>"2018-10-24T00:00:00.000Z", :sma=>218.76}, + {:date_time=>"2018-10-23T00:00:00.000Z", :sma=>219.97999999999996}, + {:date_time=>"2018-10-22T00:00:00.000Z", :sma=>219.86400000000003}, + {:date_time=>"2018-10-19T00:00:00.000Z", :sma=>219.206}, + {:date_time=>"2018-10-18T00:00:00.000Z", :sma=>219.766}, + {:date_time=>"2018-10-17T00:00:00.000Z", :sma=>219.452}, + {:date_time=>"2018-10-16T00:00:00.000Z", :sma=>218.48600000000002}, + {:date_time=>"2018-10-15T00:00:00.000Z", :sma=>219.43} + ] + + expect(normalized_output).to eq(expected_output) + end + end + end +end diff --git a/spec/technical_analysis/indicators/kc_spec.rb b/spec/technical_analysis/indicators/kc_spec.rb new file mode 100644 index 0000000..423002a --- /dev/null +++ b/spec/technical_analysis/indicators/kc_spec.rb @@ -0,0 +1,110 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "KC" do + input_data = SpecHelper.get_test_data(:high, :low, :close) + indicator = TechnicalAnalysis::Kc + + describe 'Keltner Channel' do + it 'Calculates KC (10 day)' do + output = indicator.calculate(input_data, period: 10) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :lower_band=>147.1630066666667, :middle_band=>151.9909966666667, :upper_band=>156.8189866666667}, + {:date_time=>"2019-01-08T00:00:00.000Z", :lower_band=>146.74034, :middle_band=>151.57433, :upper_band=>156.40832}, + {:date_time=>"2019-01-07T00:00:00.000Z", :lower_band=>146.46500666666665, :middle_band=>151.82199666666665, :upper_band=>157.17898666666665}, + {:date_time=>"2019-01-04T00:00:00.000Z", :lower_band=>147.12967333333333, :middle_band=>152.87466333333333, :upper_band=>158.61965333333333}, + {:date_time=>"2019-01-03T00:00:00.000Z", :lower_band=>148.32933333333335, :middle_band=>154.43533333333335, :upper_band=>160.54133333333334}, + {:date_time=>"2019-01-02T00:00:00.000Z", :lower_band=>150.65666666666667, :middle_band=>156.70466666666667, :upper_band=>162.75266666666667}, + {:date_time=>"2018-12-31T00:00:00.000Z", :lower_band=>151.35733333333334, :middle_band=>157.50533333333334, :upper_band=>163.65333333333334}, + {:date_time=>"2018-12-28T00:00:00.000Z", :lower_band=>152.14066666666668, :middle_band=>158.38066666666668, :upper_band=>164.6206666666667}, + {:date_time=>"2018-12-27T00:00:00.000Z", :lower_band=>153.69466666666665, :middle_band=>159.83966666666666, :upper_band=>165.98466666666667}, + {:date_time=>"2018-12-26T00:00:00.000Z", :lower_band=>155.643, :middle_band=>161.408, :upper_band=>167.17299999999997}, + {:date_time=>"2018-12-24T00:00:00.000Z", :lower_band=>157.75833333333333, :middle_band=>162.9513333333333, :upper_band=>168.1443333333333}, + {:date_time=>"2018-12-21T00:00:00.000Z", :lower_band=>159.51333333333332, :middle_band=>164.8863333333333, :upper_band=>170.2593333333333}, + {:date_time=>"2018-12-20T00:00:00.000Z", :lower_band=>161.50599999999997, :middle_band=>166.64499999999998, :upper_band=>171.784}, + {:date_time=>"2018-12-19T00:00:00.000Z", :lower_band=>163.27366666666666, :middle_band=>168.16766666666666, :upper_band=>173.06166666666667}, + {:date_time=>"2018-12-18T00:00:00.000Z", :lower_band=>165.09500666666668, :middle_band=>169.76499666666666, :upper_band=>174.43498666666665}, + {:date_time=>"2018-12-17T00:00:00.000Z", :lower_band=>166.80200666666667, :middle_band=>171.53099666666668, :upper_band=>176.2599866666667}, + {:date_time=>"2018-12-14T00:00:00.000Z", :lower_band=>168.39800666666667, :middle_band=>172.89499666666666, :upper_band=>177.39198666666664}, + {:date_time=>"2018-12-13T00:00:00.000Z", :lower_band=>169.60834, :middle_band=>174.23533, :upper_band=>178.86232}, + {:date_time=>"2018-12-12T00:00:00.000Z", :lower_band=>170.07734, :middle_band=>175.03833, :upper_band=>179.99932}, + {:date_time=>"2018-12-11T00:00:00.000Z", :lower_band=>170.30667333333332, :middle_band=>175.36666333333332, :upper_band=>180.42665333333332}, + {:date_time=>"2018-12-10T00:00:00.000Z", :lower_band=>170.73033999999998, :middle_band=>175.78033, :upper_band=>180.83032}, + {:date_time=>"2018-12-07T00:00:00.000Z", :lower_band=>171.55567333333335, :middle_band=>176.37916333333334, :upper_band=>181.20265333333333}, + {:date_time=>"2018-12-06T00:00:00.000Z", :lower_band=>172.54667333333333, :middle_band=>177.12316333333334, :upper_band=>181.69965333333334}, + {:date_time=>"2018-12-04T00:00:00.000Z", :lower_band=>172.85467333333332, :middle_band=>177.59116333333333, :upper_band=>182.32765333333333}, + {:date_time=>"2018-12-03T00:00:00.000Z", :lower_band=>173.76899999999998, :middle_band=>178.4645, :upper_band=>183.16}, + {:date_time=>"2018-11-30T00:00:00.000Z", :lower_band=>174.4907, :middle_band=>179.36415, :upper_band=>184.2376}, + {:date_time=>"2018-11-29T00:00:00.000Z", :lower_band=>175.45836666666665, :middle_band=>180.50881666666666, :upper_band=>185.55926666666667}, + {:date_time=>"2018-11-28T00:00:00.000Z", :lower_band=>176.01870000000002, :middle_band=>181.41415, :upper_band=>186.8096}, + {:date_time=>"2018-11-27T00:00:00.000Z", :lower_band=>177.53838, :middle_band=>182.87081999999998, :upper_band=>188.20325999999997}, + {:date_time=>"2018-11-26T00:00:00.000Z", :lower_band=>179.58538000000001, :middle_band=>185.13482000000002, :upper_band=>190.68426000000002}, + {:date_time=>"2018-11-23T00:00:00.000Z", :lower_band=>182.7750466666667, :middle_band=>188.23148666666668, :upper_band=>193.68792666666667}, + {:date_time=>"2018-11-21T00:00:00.000Z", :lower_band=>186.36671333333337, :middle_band=>191.71065333333337, :upper_band=>197.05459333333337}, + {:date_time=>"2018-11-20T00:00:00.000Z", :lower_band=>189.16371333333333, :middle_band=>194.72865333333334, :upper_band=>200.29359333333335}, + {:date_time=>"2018-11-19T00:00:00.000Z", :lower_band=>191.99738, :middle_band=>197.26932, :upper_band=>202.54126}, + {:date_time=>"2018-11-16T00:00:00.000Z", :lower_band=>193.36638000000002, :middle_band=>198.68932, :upper_band=>204.01226}, + {:date_time=>"2018-11-15T00:00:00.000Z", :lower_band=>194.71534666666665, :middle_band=>200.30933666666664, :upper_band=>205.90332666666663}, + {:date_time=>"2018-11-14T00:00:00.000Z", :lower_band=>197.70434666666668, :middle_band=>203.34633666666667, :upper_band=>208.98832666666667}, + {:date_time=>"2018-11-13T00:00:00.000Z", :lower_band=>201.13368, :middle_band=>206.30367, :upper_band=>211.47366000000002}, + {:date_time=>"2018-11-12T00:00:00.000Z", :lower_band=>203.012, :middle_band=>208.2, :upper_band=>213.38799999999998}, + {:date_time=>"2018-11-09T00:00:00.000Z", :lower_band=>203.93166666666667, :middle_band=>209.87366666666668, :upper_band=>215.8156666666667}, + {:date_time=>"2018-11-08T00:00:00.000Z", :lower_band=>204.77, :middle_band=>211.08800000000002, :upper_band=>217.40600000000003}, + {:date_time=>"2018-11-07T00:00:00.000Z", :lower_band=>205.72966666666667, :middle_band=>212.17366666666666, :upper_band=>218.61766666666665}, + {:date_time=>"2018-11-06T00:00:00.000Z", :lower_band=>206.34433333333334, :middle_band=>213.16433333333333, :upper_band=>219.98433333333332}, + {:date_time=>"2018-11-05T00:00:00.000Z", :lower_band=>207.47566666666665, :middle_band=>214.84766666666664, :upper_band=>222.21966666666663}, + {:date_time=>"2018-11-02T00:00:00.000Z", :lower_band=>209.61566666666667, :middle_band=>216.80766666666668, :upper_band=>223.99966666666668}, + {:date_time=>"2018-11-01T00:00:00.000Z", :lower_band=>211.10266666666666, :middle_band=>217.85566666666668, :upper_band=>224.6086666666667}, + {:date_time=>"2018-10-31T00:00:00.000Z", :lower_band=>210.5626666666667, :middle_band=>217.4346666666667, :upper_band=>224.30666666666667}, + {:date_time=>"2018-10-30T00:00:00.000Z", :lower_band=>210.85700000000003, :middle_band=>217.67600000000002, :upper_band=>224.495}, + {:date_time=>"2018-10-29T00:00:00.000Z", :lower_band=>211.63036000000005, :middle_band=>218.48109000000005, :upper_band=>225.33182000000005}, + {:date_time=>"2018-10-26T00:00:00.000Z", :lower_band=>213.14902666666669, :middle_band=>219.0957566666667, :upper_band=>225.0424866666667}, + {:date_time=>"2018-10-25T00:00:00.000Z", :lower_band=>213.71936000000002, :middle_band=>219.51809000000003, :upper_band=>225.31682000000004}, + {:date_time=>"2018-10-24T00:00:00.000Z", :lower_band=>213.0756933333333, :middle_band=>219.1294233333333, :upper_band=>225.1831533333333}, + {:date_time=>"2018-10-23T00:00:00.000Z", :lower_band=>213.17802666666663, :middle_band=>219.29275666666663, :upper_band=>225.40748666666664}, + {:date_time=>"2018-10-22T00:00:00.000Z", :lower_band=>214.05418666666668, :middle_band=>219.8162966666667, :upper_band=>225.5784066666667} + ] + + 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)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('kc') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Keltner Channel') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period)) + end + + it 'Validates options' do + valid_options = { period: 22 } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(4) + end + end + end +end diff --git a/spec/technical_analysis/indicators/kst_spec.rb b/spec/technical_analysis/indicators/kst_spec.rb new file mode 100644 index 0000000..44386fd --- /dev/null +++ b/spec/technical_analysis/indicators/kst_spec.rb @@ -0,0 +1,78 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "KST" do + input_data = SpecHelper.get_test_data(:close) + indicator = TechnicalAnalysis::Kst + + describe 'Know Sure Thing' do + it 'Calculates KST' do + output = indicator.calculate(input_data, roc1: 10, roc2: 15, roc3: 20, roc4: 30, sma1: 10, sma2: 10, sma3: 10, sma4: 15, price_key: :close) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :kst=>-140.9140022298261}, + {:date_time=>"2019-01-08T00:00:00.000Z", :kst=>-148.9261153101682}, + {:date_time=>"2019-01-07T00:00:00.000Z", :kst=>-155.71040741442587}, + {:date_time=>"2019-01-04T00:00:00.000Z", :kst=>-157.83675223915662}, + {:date_time=>"2019-01-03T00:00:00.000Z", :kst=>-157.0260814891967}, + {:date_time=>"2019-01-02T00:00:00.000Z", :kst=>-150.77021075475108}, + {:date_time=>"2018-12-31T00:00:00.000Z", :kst=>-152.43337072156913}, + {:date_time=>"2018-12-28T00:00:00.000Z", :kst=>-154.278039839607}, + {:date_time=>"2018-12-27T00:00:00.000Z", :kst=>-152.69243922992774}, + {:date_time=>"2018-12-26T00:00:00.000Z", :kst=>-151.25248412993977}, + {:date_time=>"2018-12-24T00:00:00.000Z", :kst=>-150.769274028613}, + {:date_time=>"2018-12-21T00:00:00.000Z", :kst=>-145.9029270207904}, + {:date_time=>"2018-12-20T00:00:00.000Z", :kst=>-143.4025404878081}, + {:date_time=>"2018-12-19T00:00:00.000Z", :kst=>-141.34365936138443}, + {:date_time=>"2018-12-18T00:00:00.000Z", :kst=>-141.36803679622636}, + {:date_time=>"2018-12-17T00:00:00.000Z", :kst=>-140.69442915235626}, + {:date_time=>"2018-12-14T00:00:00.000Z", :kst=>-141.6235541026754}, + {:date_time=>"2018-12-13T00:00:00.000Z", :kst=>-143.06704590371226}, + {:date_time=>"2018-12-12T00:00:00.000Z", :kst=>-146.67417088368043}, + {:date_time=>"2018-12-11T00:00:00.000Z", :kst=>-150.14198896419614} + ] + + expect(normalized_output).to eq(expected_output) + end + + it "Throws exception if not enough data" do + roc4 = 60 + sma4 = 30 + expect {indicator.calculate(input_data, roc4: roc4, sma4: sma4, 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('kst') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Know Sure Thing') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period roc1 roc2 roc3 roc4 sma1 sma2 sma3 sma4 price_key)) + end + + it 'Validates options' do + valid_options = { roc1: 10, roc2: 15, roc3: 20, roc4: 30, sma1: 10, sma2: 10, sma3: 10, sma4: 15, price_key: :kst } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { roc4: 30, sma4: 15 } + expect(indicator.min_data_size(options)).to eq(44) + end + end + end +end diff --git a/spec/technical_analysis/indicators/macd_spec.rb b/spec/technical_analysis/indicators/macd_spec.rb new file mode 100644 index 0000000..5e79766 --- /dev/null +++ b/spec/technical_analysis/indicators/macd_spec.rb @@ -0,0 +1,86 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "MACD" do + input_data = SpecHelper.get_test_data(:close) + indicator = TechnicalAnalysis::Macd + + describe 'Moving Average Convergence Divergence' do + it 'Calculates MACD (12, 26, 9)' do + output = indicator.calculate(input_data, fast_period: 12, slow_period: 26, signal_period: 9, price_key: :close) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :macd_histogram=>0.8762597178840466, :macd_line=>-8.126908458242355, :signal_line=>-9.003168176126401}, + {:date_time=>"2019-01-08T00:00:00.000Z", :macd_histogram=>0.4770591535283888, :macd_line=>-8.745173952069024, :signal_line=>-9.222233105597413}, + {:date_time=>"2019-01-07T00:00:00.000Z", :macd_histogram=>0.19504942363819744, :macd_line=>-9.146448470341312, :signal_line=>-9.34149789397951}, + {:date_time=>"2019-01-04T00:00:00.000Z", :macd_histogram=>0.15180609943062073, :macd_line=>-9.238454150458438, :signal_line=>-9.390260249889058}, + {:date_time=>"2019-01-03T00:00:00.000Z", :macd_histogram=>0.17310243476363496, :macd_line=>-9.255109339983079, :signal_line=>-9.428211774746714}, + {:date_time=>"2019-01-02T00:00:00.000Z", :macd_histogram=>0.9477761010428516, :macd_line=>-8.523711282394771, :signal_line=>-9.471487383437623}, + {:date_time=>"2018-12-31T00:00:00.000Z", :macd_histogram=>0.6406312657329245, :macd_line=>-9.06780014296541, :signal_line=>-9.708431408698335}, + {:date_time=>"2018-12-28T00:00:00.000Z", :macd_histogram=>0.25655639311731626, :macd_line=>-9.61203283201425, :signal_line=>-9.868589225131567}, + {:date_time=>"2018-12-27T00:00:00.000Z", :macd_histogram=>-0.0803864814327433, :macd_line=>-10.013114804843639, :signal_line=>-9.932728323410895}, + {:date_time=>"2018-12-26T00:00:00.000Z", :macd_histogram=>-0.45861878631368036, :macd_line=>-10.371250489366389, :signal_line=>-9.912631703052709}, + {:date_time=>"2018-12-24T00:00:00.000Z", :macd_histogram=>-0.9833848403830618, :macd_line=>-10.78136184685735, :signal_line=>-9.797977006474289}, + {:date_time=>"2018-12-21T00:00:00.000Z", :macd_histogram=>-0.5430624653584708, :macd_line=>-10.095193261736995, :signal_line=>-9.552130796378524}, + {:date_time=>"2018-12-20T00:00:00.000Z", :macd_histogram=>-0.053279185610067614, :macd_line=>-9.469644365648975, :signal_line=>-9.416365180038907}, + {:date_time=>"2018-12-19T00:00:00.000Z", :macd_histogram=>0.24847825431282367, :macd_line=>-9.154567129323567, :signal_line=>-9.40304538363639}, + {:date_time=>"2018-12-18T00:00:00.000Z", :macd_histogram=>0.4325264668638127, :macd_line=>-9.032638480350784, :signal_line=>-9.465164947214596}, + {:date_time=>"2018-12-17T00:00:00.000Z", :macd_histogram=>0.30024902798283826, :macd_line=>-9.273047535947711, :signal_line=>-9.57329656393055}, + {:date_time=>"2018-12-14T00:00:00.000Z", :macd_histogram=>0.420215327266785, :macd_line=>-9.228143493659474, :signal_line=>-9.648358820926259}, + {:date_time=>"2018-12-13T00:00:00.000Z", :macd_histogram=>0.5600105875119272, :macd_line=>-9.193402065231027, :signal_line=>-9.753412652742954}, + {:date_time=>"2018-12-12T00:00:00.000Z", :macd_histogram=>0.3211326876237397, :macd_line=>-9.572282611997196, :signal_line=>-9.893415299620935}, + {:date_time=>"2018-12-11T00:00:00.000Z", :macd_histogram=>0.24542334510443808, :macd_line=>-9.728275126422432, :signal_line=>-9.97369847152687}, + {:date_time=>"2018-12-10T00:00:00.000Z", :macd_histogram=>0.29703273753484893, :macd_line=>-9.738021570268131, :signal_line=>-10.03505430780298}, + {:date_time=>"2018-12-07T00:00:00.000Z", :macd_histogram=>0.40173122915939885, :macd_line=>-9.707581263027294, :signal_line=>-10.109312492186692}, + {:date_time=>"2018-12-06T00:00:00.000Z", :macd_histogram=>0.7952363015170398, :macd_line=>-9.414508997959501, :signal_line=>-10.209745299476541}, + {:date_time=>"2018-12-04T00:00:00.000Z", :macd_histogram=>0.8707231988258766, :macd_line=>-9.537831176029925, :signal_line=>-10.408554374855802}, + {:date_time=>"2018-12-03T00:00:00.000Z", :macd_histogram=>0.8691503265374259, :macd_line=>-9.757084848024846, :signal_line=>-10.626235174562272}, + {:date_time=>"2018-11-30T00:00:00.000Z", :macd_histogram=>0.12072991006954936, :macd_line=>-10.722792846127078, :signal_line=>-10.843522756196627}, + {:date_time=>"2018-11-29T00:00:00.000Z", :macd_histogram=>-0.29036883404714864, :macd_line=>-11.164074067761163, :signal_line=>-10.873705233714015}, + {:date_time=>"2018-11-28T00:00:00.000Z", :macd_histogram=>-0.8625734517317483, :macd_line=>-11.663686476933975, :signal_line=>-10.801113025202227}, + {:date_time=>"2018-11-27T00:00:00.000Z", :macd_histogram=>-1.6877775772534704, :macd_line=>-12.27324723952276, :signal_line=>-10.58546966226929}, + {:date_time=>"2018-11-26T00:00:00.000Z", :macd_histogram=>-2.0270413850598707, :macd_line=>-12.190566653015793, :signal_line=>-10.163525267955922} + ] + + expect(normalized_output).to eq(expected_output) + end + + it "Throws exception if not enough data" do + expect {indicator.calculate(input_data, slow_period: input_data.size+1, signal_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('macd') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Moving Average Convergence Divergence') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(fast_period slow_period signal_period price_key)) + end + + it 'Validates options' do + valid_options = { fast_period: 12, slow_period: 26, signal_period: 9, price_key: :close } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { fast_period: 12, slow_period: 24, signal_period: 10, price_key: :close } + expect(indicator.min_data_size(options)).to eq(33) + end + end + end +end diff --git a/spec/technical_analysis/indicators/mfi_spec.rb b/spec/technical_analysis/indicators/mfi_spec.rb new file mode 100644 index 0000000..9b46e53 --- /dev/null +++ b/spec/technical_analysis/indicators/mfi_spec.rb @@ -0,0 +1,105 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "MFI" do + input_data = SpecHelper.get_test_data(:high, :low, :close, :volume) + indicator = TechnicalAnalysis::Mfi + + describe 'Money Flow Index' do + it 'Calculates MFI (14 day)' do + output = indicator.calculate(input_data, period: 14) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :mfi=>50.72343663578981}, + {:date_time=>"2019-01-08T00:00:00.000Z", :mfi=>50.1757147722236}, + {:date_time=>"2019-01-07T00:00:00.000Z", :mfi=>44.40424662233335}, + {:date_time=>"2019-01-04T00:00:00.000Z", :mfi=>37.81171947764313}, + {:date_time=>"2019-01-03T00:00:00.000Z", :mfi=>36.12141791144509}, + {:date_time=>"2019-01-02T00:00:00.000Z", :mfi=>44.40322811326429}, + {:date_time=>"2018-12-31T00:00:00.000Z", :mfi=>50.84666407662511}, + {:date_time=>"2018-12-28T00:00:00.000Z", :mfi=>43.66248537462054}, + {:date_time=>"2018-12-27T00:00:00.000Z", :mfi=>37.56125308902594}, + {:date_time=>"2018-12-26T00:00:00.000Z", :mfi=>30.54886108664553}, + {:date_time=>"2018-12-24T00:00:00.000Z", :mfi=>22.795547568232564}, + {:date_time=>"2018-12-21T00:00:00.000Z", :mfi=>29.111701049682864}, + {:date_time=>"2018-12-20T00:00:00.000Z", :mfi=>31.23926122664477}, + {:date_time=>"2018-12-19T00:00:00.000Z", :mfi=>39.506183778454634}, + {:date_time=>"2018-12-18T00:00:00.000Z", :mfi=>47.447534147092476}, + {:date_time=>"2018-12-17T00:00:00.000Z", :mfi=>48.23034273673764}, + {:date_time=>"2018-12-14T00:00:00.000Z", :mfi=>47.94902504034211}, + {:date_time=>"2018-12-13T00:00:00.000Z", :mfi=>49.219385053554596}, + {:date_time=>"2018-12-12T00:00:00.000Z", :mfi=>43.77747404533765}, + {:date_time=>"2018-12-11T00:00:00.000Z", :mfi=>35.65846689890073}, + {:date_time=>"2018-12-10T00:00:00.000Z", :mfi=>28.368262151706787}, + {:date_time=>"2018-12-07T00:00:00.000Z", :mfi=>36.043235982173}, + {:date_time=>"2018-12-06T00:00:00.000Z", :mfi=>43.83608665179891}, + {:date_time=>"2018-12-04T00:00:00.000Z", :mfi=>42.20999531156955}, + {:date_time=>"2018-12-03T00:00:00.000Z", :mfi=>41.56028762200471}, + {:date_time=>"2018-11-30T00:00:00.000Z", :mfi=>34.05798943239071}, + {:date_time=>"2018-11-29T00:00:00.000Z", :mfi=>34.06816082393442}, + {:date_time=>"2018-11-28T00:00:00.000Z", :mfi=>32.76065853310956}, + {:date_time=>"2018-11-27T00:00:00.000Z", :mfi=>31.964707056845796}, + {:date_time=>"2018-11-26T00:00:00.000Z", :mfi=>31.547361595662196}, + {:date_time=>"2018-11-23T00:00:00.000Z", :mfi=>30.0162422598642}, + {:date_time=>"2018-11-21T00:00:00.000Z", :mfi=>26.562703378271138}, + {:date_time=>"2018-11-20T00:00:00.000Z", :mfi=>33.960148214906965}, + {:date_time=>"2018-11-19T00:00:00.000Z", :mfi=>41.216526023725}, + {:date_time=>"2018-11-16T00:00:00.000Z", :mfi=>41.22749344358184}, + {:date_time=>"2018-11-15T00:00:00.000Z", :mfi=>35.21136683265594}, + {:date_time=>"2018-11-14T00:00:00.000Z", :mfi=>28.384945303826555}, + {:date_time=>"2018-11-13T00:00:00.000Z", :mfi=>34.34229761188338}, + {:date_time=>"2018-11-12T00:00:00.000Z", :mfi=>34.42914208336833}, + {:date_time=>"2018-11-09T00:00:00.000Z", :mfi=>34.82129345577579}, + {:date_time=>"2018-11-08T00:00:00.000Z", :mfi=>39.93886476277229}, + {:date_time=>"2018-11-07T00:00:00.000Z", :mfi=>40.83081970751587}, + {:date_time=>"2018-11-06T00:00:00.000Z", :mfi=>35.50426811855657}, + {:date_time=>"2018-11-05T00:00:00.000Z", :mfi=>34.78216906343063}, + {:date_time=>"2018-11-02T00:00:00.000Z", :mfi=>41.95906812943111}, + {:date_time=>"2018-11-01T00:00:00.000Z", :mfi=>46.69168911629936}, + {:date_time=>"2018-10-31T00:00:00.000Z", :mfi=>45.214622261116865}, + {:date_time=>"2018-10-30T00:00:00.000Z", :mfi=>36.38768375444253}, + {:date_time=>"2018-10-29T00:00:00.000Z", :mfi=>35.973926377719295} + ] + + 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+2)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('mfi') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Money Flow Index') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period)) + end + + it 'Validates options' do + valid_options = { period: 22 } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(5) + end + end + end +end diff --git a/spec/technical_analysis/indicators/mi_spec.rb b/spec/technical_analysis/indicators/mi_spec.rb new file mode 100644 index 0000000..37ceec6 --- /dev/null +++ b/spec/technical_analysis/indicators/mi_spec.rb @@ -0,0 +1,79 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "MI" do + input_data = SpecHelper.get_test_data(:high, :low) + indicator = TechnicalAnalysis::Mi + + describe 'Simple Mass Index' do + it 'Calculates MI' do + output = indicator.calculate(input_data, ema_period: 9, sum_period: 25) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :mi=>24.77520633216394}, + {:date_time=>"2019-01-08T00:00:00.000Z", :mi=>24.80084030980544}, + {:date_time=>"2019-01-07T00:00:00.000Z", :mi=>24.924292786436485}, + {:date_time=>"2019-01-04T00:00:00.000Z", :mi=>25.026285600546654}, + {:date_time=>"2019-01-03T00:00:00.000Z", :mi=>25.018142841959207}, + {:date_time=>"2019-01-02T00:00:00.000Z", :mi=>25.04245599370965}, + {:date_time=>"2018-12-31T00:00:00.000Z", :mi=>25.03284918462693}, + {:date_time=>"2018-12-28T00:00:00.000Z", :mi=>25.020764664334674}, + {:date_time=>"2018-12-27T00:00:00.000Z", :mi=>24.964776002066408}, + {:date_time=>"2018-12-26T00:00:00.000Z", :mi=>24.791003528125515}, + {:date_time=>"2018-12-24T00:00:00.000Z", :mi=>24.564590708470064}, + {:date_time=>"2018-12-21T00:00:00.000Z", :mi=>24.454674020826847}, + {:date_time=>"2018-12-20T00:00:00.000Z", :mi=>24.303447406952383}, + {:date_time=>"2018-12-19T00:00:00.000Z", :mi=>24.12156756268421}, + {:date_time=>"2018-12-18T00:00:00.000Z", :mi=>23.94958830559542}, + {:date_time=>"2018-12-17T00:00:00.000Z", :mi=>23.879826345759078}, + {:date_time=>"2018-12-14T00:00:00.000Z", :mi=>23.792012619835983}, + {:date_time=>"2018-12-13T00:00:00.000Z", :mi=>23.82241708019551}, + {:date_time=>"2018-12-12T00:00:00.000Z", :mi=>23.835760161850434}, + {:date_time=>"2018-12-11T00:00:00.000Z", :mi=>23.89261610689666}, + {:date_time=>"2018-12-10T00:00:00.000Z", :mi=>23.890966368346767}, + {:date_time=>"2018-12-07T00:00:00.000Z", :mi=>23.8163614080134}, + {:date_time=>"2018-12-06T00:00:00.000Z", :mi=>23.82917406071097} + ] + + expect(normalized_output).to eq(expected_output) + end + + it "Throws exception if not enough data" do + expect {indicator.calculate(input_data, ema_period: input_data.size+1)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('mi') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Mass Index') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(ema_period sum_period)) + end + + it 'Validates options' do + valid_options = { ema_period: 9, sum_period: 25 } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { ema_period: 10, sum_period: 20 } + expect(indicator.min_data_size(options)).to eq(38) + end + end + end +end diff --git a/spec/technical_analysis/indicators/nvi_spec.rb b/spec/technical_analysis/indicators/nvi_spec.rb new file mode 100644 index 0000000..9b63de2 --- /dev/null +++ b/spec/technical_analysis/indicators/nvi_spec.rb @@ -0,0 +1,119 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "NVI" do + input_data = SpecHelper.get_test_data(:close, :volume) + indicator = TechnicalAnalysis::Nvi + + describe 'Negative Volume Index' do + it 'Calculates NVI' do + output = indicator.calculate(input_data) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :nvi=>1002.8410612825647}, + {:date_time=>"2019-01-08T00:00:00.000Z", :nvi=>1002.8410612825647}, + {:date_time=>"2019-01-07T00:00:00.000Z", :nvi=>1000.9347542454526}, + {:date_time=>"2019-01-04T00:00:00.000Z", :nvi=>1001.1573361960799}, + {:date_time=>"2019-01-03T00:00:00.000Z", :nvi=>996.888400265283}, + {:date_time=>"2019-01-02T00:00:00.000Z", :nvi=>996.888400265283}, + {:date_time=>"2018-12-31T00:00:00.000Z", :nvi=>996.888400265283}, + {:date_time=>"2018-12-28T00:00:00.000Z", :nvi=>995.9218765502475}, + {:date_time=>"2018-12-27T00:00:00.000Z", :nvi=>995.8706437612625}, + {:date_time=>"2018-12-26T00:00:00.000Z", :nvi=>996.519622574013}, + {:date_time=>"2018-12-24T00:00:00.000Z", :nvi=>996.519622574013}, + {:date_time=>"2018-12-21T00:00:00.000Z", :nvi=>999.1070305219995}, + {:date_time=>"2018-12-20T00:00:00.000Z", :nvi=>999.1070305219995}, + {:date_time=>"2018-12-19T00:00:00.000Z", :nvi=>999.1070305219995}, + {:date_time=>"2018-12-18T00:00:00.000Z", :nvi=>999.1070305219995}, + {:date_time=>"2018-12-17T00:00:00.000Z", :nvi=>997.8077746966976}, + {:date_time=>"2018-12-14T00:00:00.000Z", :nvi=>997.8077746966976}, + {:date_time=>"2018-12-13T00:00:00.000Z", :nvi=>997.8077746966976}, + {:date_time=>"2018-12-12T00:00:00.000Z", :nvi=>996.713747493859}, + {:date_time=>"2018-12-11T00:00:00.000Z", :nvi=>996.4350307767862}, + {:date_time=>"2018-12-10T00:00:00.000Z", :nvi=>997.0069647390503}, + {:date_time=>"2018-12-07T00:00:00.000Z", :nvi=>997.0069647390503}, + {:date_time=>"2018-12-06T00:00:00.000Z", :nvi=>1000.5726698672554}, + {:date_time=>"2018-12-04T00:00:00.000Z", :nvi=>1000.5726698672554}, + {:date_time=>"2018-12-03T00:00:00.000Z", :nvi=>1000.5726698672554}, + {:date_time=>"2018-11-30T00:00:00.000Z", :nvi=>1000.5726698672554}, + {:date_time=>"2018-11-29T00:00:00.000Z", :nvi=>1001.1129093548633}, + {:date_time=>"2018-11-28T00:00:00.000Z", :nvi=>1001.8811198113682}, + {:date_time=>"2018-11-27T00:00:00.000Z", :nvi=>1001.8811198113682}, + {:date_time=>"2018-11-26T00:00:00.000Z", :nvi=>1002.0987352047939}, + {:date_time=>"2018-11-23T00:00:00.000Z", :nvi=>1002.0987352047939}, + {:date_time=>"2018-11-21T00:00:00.000Z", :nvi=>1004.6386152817257}, + {:date_time=>"2018-11-20T00:00:00.000Z", :nvi=>1004.7516224011742}, + {:date_time=>"2018-11-19T00:00:00.000Z", :nvi=>1004.7516224011742}, + {:date_time=>"2018-11-16T00:00:00.000Z", :nvi=>1004.7516224011742}, + {:date_time=>"2018-11-15T00:00:00.000Z", :nvi=>1003.6440522637729}, + {:date_time=>"2018-11-14T00:00:00.000Z", :nvi=>1001.1761721781198}, + {:date_time=>"2018-11-13T00:00:00.000Z", :nvi=>1001.1761721781198}, + {:date_time=>"2018-11-12T00:00:00.000Z", :nvi=>1002.1752966566695}, + {:date_time=>"2018-11-09T00:00:00.000Z", :nvi=>1002.1752966566695}, + {:date_time=>"2018-11-08T00:00:00.000Z", :nvi=>1002.1752966566695}, + {:date_time=>"2018-11-07T00:00:00.000Z", :nvi=>1002.8707003242093}, + {:date_time=>"2018-11-06T00:00:00.000Z", :nvi=>1002.8707003242093}, + {:date_time=>"2018-11-05T00:00:00.000Z", :nvi=>1001.7892974768458}, + {:date_time=>"2018-11-02T00:00:00.000Z", :nvi=>1004.6281253156736}, + {:date_time=>"2018-11-01T00:00:00.000Z", :nvi=>1004.6281253156736}, + {:date_time=>"2018-10-31T00:00:00.000Z", :nvi=>1004.6281253156736}, + {:date_time=>"2018-10-30T00:00:00.000Z", :nvi=>1004.6281253156736}, + {:date_time=>"2018-10-29T00:00:00.000Z", :nvi=>1004.1286907133366}, + {:date_time=>"2018-10-26T00:00:00.000Z", :nvi=>1006.0057133670583}, + {:date_time=>"2018-10-25T00:00:00.000Z", :nvi=>1006.0057133670583}, + {:date_time=>"2018-10-24T00:00:00.000Z", :nvi=>1003.8159323451605}, + {:date_time=>"2018-10-23T00:00:00.000Z", :nvi=>1003.8159323451605}, + {:date_time=>"2018-10-22T00:00:00.000Z", :nvi=>1003.8159323451605}, + {:date_time=>"2018-10-19T00:00:00.000Z", :nvi=>1003.2049250951491}, + {:date_time=>"2018-10-18T00:00:00.000Z", :nvi=>1003.2049250951491}, + {:date_time=>"2018-10-17T00:00:00.000Z", :nvi=>1003.2049250951491}, + {:date_time=>"2018-10-16T00:00:00.000Z", :nvi=>1003.6370655407939}, + {:date_time=>"2018-10-15T00:00:00.000Z", :nvi=>1001.4333482054976}, + {:date_time=>"2018-10-12T00:00:00.000Z", :nvi=>1003.5719281883889}, + {:date_time=>"2018-10-11T00:00:00.000Z", :nvi=>1000.0}, + {:date_time=>"2018-10-10T00:00:00.000Z", :nvi=>1000.0}, + {:date_time=>"2018-10-09T00:00:00.000Z", :nvi=>1000.0} + ] + + expect(normalized_output).to eq(expected_output) + end + + it "Throws exception if not enough data" do + expect {indicator.calculate([])}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('nvi') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Negative Volume Index') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq([]) + end + + it 'Validates options' do + valid_options = {} + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = {} + expect(indicator.min_data_size(options)).to eq(1) + end + end + end +end diff --git a/spec/technical_analysis/indicators/obv_mean_spec.rb b/spec/technical_analysis/indicators/obv_mean_spec.rb new file mode 100644 index 0000000..d4edd4b --- /dev/null +++ b/spec/technical_analysis/indicators/obv_mean_spec.rb @@ -0,0 +1,109 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "OBV Mean" do + input_data = SpecHelper.get_test_data(:close, :volume) + indicator = TechnicalAnalysis::ObvMean + + describe 'On-balance Volume Mean' do + it 'Calculates OBV Mean (10 day)' do + output = indicator.calculate(input_data, period: 10) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :obv_mean=>-642606913.0}, + {:date_time=>"2019-01-08T00:00:00.000Z", :obv_mean=>-654187384.0}, + {:date_time=>"2019-01-07T00:00:00.000Z", :obv_mean=>-657547495.0}, + {:date_time=>"2019-01-04T00:00:00.000Z", :obv_mean=>-647295525.0}, + {:date_time=>"2019-01-03T00:00:00.000Z", :obv_mean=>-636060876.0}, + {:date_time=>"2019-01-02T00:00:00.000Z", :obv_mean=>-614324095.0}, + {:date_time=>"2018-12-31T00:00:00.000Z", :obv_mean=>-605073347.0}, + {:date_time=>"2018-12-28T00:00:00.000Z", :obv_mean=>-587933850.0}, + {:date_time=>"2018-12-27T00:00:00.000Z", :obv_mean=>-563282378.0}, + {:date_time=>"2018-12-26T00:00:00.000Z", :obv_mean=>-537632267.0}, + {:date_time=>"2018-12-24T00:00:00.000Z", :obv_mean=>-520690509.0}, + {:date_time=>"2018-12-21T00:00:00.000Z", :obv_mean=>-493338562.0}, + {:date_time=>"2018-12-20T00:00:00.000Z", :obv_mean=>-475879438.0}, + {:date_time=>"2018-12-19T00:00:00.000Z", :obv_mean=>-463802236.0}, + {:date_time=>"2018-12-18T00:00:00.000Z", :obv_mean=>-453894366.0}, + {:date_time=>"2018-12-17T00:00:00.000Z", :obv_mean=>-444632138.0}, + {:date_time=>"2018-12-14T00:00:00.000Z", :obv_mean=>-436048331.0}, + {:date_time=>"2018-12-13T00:00:00.000Z", :obv_mean=>-427847140.0}, + {:date_time=>"2018-12-12T00:00:00.000Z", :obv_mean=>-419555627.0}, + {:date_time=>"2018-12-11T00:00:00.000Z", :obv_mean=>-412682868.0}, + {:date_time=>"2018-12-10T00:00:00.000Z", :obv_mean=>-398147027.0}, + {:date_time=>"2018-12-07T00:00:00.000Z", :obv_mean=>-392674222.0}, + {:date_time=>"2018-12-06T00:00:00.000Z", :obv_mean=>-378663120.0}, + {:date_time=>"2018-12-04T00:00:00.000Z", :obv_mean=>-365710262.0}, + {:date_time=>"2018-12-03T00:00:00.000Z", :obv_mean=>-350260027.0}, + {:date_time=>"2018-11-30T00:00:00.000Z", :obv_mean=>-334761235.0}, + {:date_time=>"2018-11-29T00:00:00.000Z", :obv_mean=>-318827806.0}, + {:date_time=>"2018-11-28T00:00:00.000Z", :obv_mean=>-311463969.0}, + {:date_time=>"2018-11-27T00:00:00.000Z", :obv_mean=>-302197756.0}, + {:date_time=>"2018-11-26T00:00:00.000Z", :obv_mean=>-283664797.0}, + {:date_time=>"2018-11-23T00:00:00.000Z", :obv_mean=>-264148349.0}, + {:date_time=>"2018-11-21T00:00:00.000Z", :obv_mean=>-236733893.0}, + {:date_time=>"2018-11-20T00:00:00.000Z", :obv_mean=>-209152907.0}, + {:date_time=>"2018-11-19T00:00:00.000Z", :obv_mean=>-188010709.0}, + {:date_time=>"2018-11-16T00:00:00.000Z", :obv_mean=>-176813851.0}, + {:date_time=>"2018-11-15T00:00:00.000Z", :obv_mean=>-163172458.0}, + {:date_time=>"2018-11-14T00:00:00.000Z", :obv_mean=>-136807276.0}, + {:date_time=>"2018-11-13T00:00:00.000Z", :obv_mean=>-111110335.0}, + {:date_time=>"2018-11-12T00:00:00.000Z", :obv_mean=>-95269809.0}, + {:date_time=>"2018-11-09T00:00:00.000Z", :obv_mean=>-87750647.0}, + {:date_time=>"2018-11-08T00:00:00.000Z", :obv_mean=>-80759219.0}, + {:date_time=>"2018-11-07T00:00:00.000Z", :obv_mean=>-72480397.0}, + {:date_time=>"2018-11-06T00:00:00.000Z", :obv_mean=>-69633236.0}, + {:date_time=>"2018-11-05T00:00:00.000Z", :obv_mean=>-59457699.0}, + {:date_time=>"2018-11-02T00:00:00.000Z", :obv_mean=>-49972807.0}, + {:date_time=>"2018-11-01T00:00:00.000Z", :obv_mean=>-49970286.0}, + {:date_time=>"2018-10-31T00:00:00.000Z", :obv_mean=>-62359854.0}, + {:date_time=>"2018-10-30T00:00:00.000Z", :obv_mean=>-66215087.0}, + {:date_time=>"2018-10-29T00:00:00.000Z", :obv_mean=>-63999351.0}, + {:date_time=>"2018-10-26T00:00:00.000Z", :obv_mean=>-61015077.0}, + {:date_time=>"2018-10-25T00:00:00.000Z", :obv_mean=>-59574127.0}, + {:date_time=>"2018-10-24T00:00:00.000Z", :obv_mean=>-66801824.0}, + {:date_time=>"2018-10-23T00:00:00.000Z", :obv_mean=>-65836555.0} + ] + + 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)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('obv_mean') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('On-balance Volume Mean') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period)) + end + + it 'Validates options' do + valid_options = { period: 22 } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(5) + end + end + end +end diff --git a/spec/technical_analysis/indicators/obv_spec.rb b/spec/technical_analysis/indicators/obv_spec.rb new file mode 100644 index 0000000..7e9eb98 --- /dev/null +++ b/spec/technical_analysis/indicators/obv_spec.rb @@ -0,0 +1,119 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "OBV" do + input_data = SpecHelper.get_test_data(:close, :volume) + indicator = TechnicalAnalysis::Obv + + describe 'On-balance Volume' do + it 'Calculates OBV' do + output = indicator.calculate(input_data) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :obv=>-591085010}, + {:date_time=>"2019-01-08T00:00:00.000Z", :obv=>-636119380}, + {:date_time=>"2019-01-07T00:00:00.000Z", :obv=>-676742290}, + {:date_time=>"2019-01-04T00:00:00.000Z", :obv=>-622170850}, + {:date_time=>"2019-01-03T00:00:00.000Z", :obv=>-679594500}, + {:date_time=>"2019-01-02T00:00:00.000Z", :obv=>-588487660}, + {:date_time=>"2018-12-31T00:00:00.000Z", :obv=>-624124730}, + {:date_time=>"2018-12-28T00:00:00.000Z", :obv=>-658624120}, + {:date_time=>"2018-12-27T00:00:00.000Z", :obv=>-700364720}, + {:date_time=>"2018-12-26T00:00:00.000Z", :obv=>-648755870}, + {:date_time=>"2018-12-24T00:00:00.000Z", :obv=>-706889720}, + {:date_time=>"2018-12-21T00:00:00.000Z", :obv=>-669720490}, + {:date_time=>"2018-12-20T00:00:00.000Z", :obv=>-574222590}, + {:date_time=>"2018-12-19T00:00:00.000Z", :obv=>-509824360}, + {:date_time=>"2018-12-18T00:00:00.000Z", :obv=>-462226690}, + {:date_time=>"2018-12-17T00:00:00.000Z", :obv=>-495980180}, + {:date_time=>"2018-12-14T00:00:00.000Z", :obv=>-452729760}, + {:date_time=>"2018-12-13T00:00:00.000Z", :obv=>-412109400}, + {:date_time=>"2018-12-12T00:00:00.000Z", :obv=>-443863610}, + {:date_time=>"2018-12-11T00:00:00.000Z", :obv=>-479338290}, + {:date_time=>"2018-12-10T00:00:00.000Z", :obv=>-433370250}, + {:date_time=>"2018-12-07T00:00:00.000Z", :obv=>-495129250}, + {:date_time=>"2018-12-06T00:00:00.000Z", :obv=>-453450570}, + {:date_time=>"2018-12-04T00:00:00.000Z", :obv=>-410745660}, + {:date_time=>"2018-12-03T00:00:00.000Z", :obv=>-369604410}, + {:date_time=>"2018-11-30T00:00:00.000Z", :obv=>-410142110}, + {:date_time=>"2018-11-29T00:00:00.000Z", :obv=>-370717850}, + {:date_time=>"2018-11-28T00:00:00.000Z", :obv=>-329194270}, + {:date_time=>"2018-11-27T00:00:00.000Z", :obv=>-375136020}, + {:date_time=>"2018-11-26T00:00:00.000Z", :obv=>-333979880}, + {:date_time=>"2018-11-23T00:00:00.000Z", :obv=>-378642200}, + {:date_time=>"2018-11-21T00:00:00.000Z", :obv=>-355018230}, + {:date_time=>"2018-11-20T00:00:00.000Z", :obv=>-323921990}, + {:date_time=>"2018-11-19T00:00:00.000Z", :obv=>-256243310}, + {:date_time=>"2018-11-16T00:00:00.000Z", :obv=>-214616490}, + {:date_time=>"2018-11-15T00:00:00.000Z", :obv=>-250807820}, + {:date_time=>"2018-11-14T00:00:00.000Z", :obv=>-297079480}, + {:date_time=>"2018-11-13T00:00:00.000Z", :obv=>-236532140}, + {:date_time=>"2018-11-12T00:00:00.000Z", :obv=>-189806430}, + {:date_time=>"2018-11-09T00:00:00.000Z", :obv=>-138815400}, + {:date_time=>"2018-11-08T00:00:00.000Z", :obv=>-104497640}, + {:date_time=>"2018-11-07T00:00:00.000Z", :obv=>-79208370}, + {:date_time=>"2018-11-06T00:00:00.000Z", :obv=>-112500010}, + {:date_time=>"2018-11-05T00:00:00.000Z", :obv=>-144274730}, + {:date_time=>"2018-11-02T00:00:00.000Z", :obv=>-78202560}, + {:date_time=>"2018-11-01T00:00:00.000Z", :obv=>12844000}, + {:date_time=>"2018-10-31T00:00:00.000Z", :obv=>-40110070}, + {:date_time=>"2018-10-30T00:00:00.000Z", :obv=>-78126880}, + {:date_time=>"2018-10-29T00:00:00.000Z", :obv=>-114614810}, + {:date_time=>"2018-10-26T00:00:00.000Z", :obv=>-68901120}, + {:date_time=>"2018-10-25T00:00:00.000Z", :obv=>-21709420}, + {:date_time=>"2018-10-24T00:00:00.000Z", :obv=>-50736760}, + {:date_time=>"2018-10-23T00:00:00.000Z", :obv=>-10744640}, + {:date_time=>"2018-10-22T00:00:00.000Z", :obv=>-49425810}, + {:date_time=>"2018-10-19T00:00:00.000Z", :obv=>-78177350}, + {:date_time=>"2018-10-18T00:00:00.000Z", :obv=>-111051680}, + {:date_time=>"2018-10-17T00:00:00.000Z", :obv=>-78662400}, + {:date_time=>"2018-10-16T00:00:00.000Z", :obv=>-55969520}, + {:date_time=>"2018-10-15T00:00:00.000Z", :obv=>-84772070}, + {:date_time=>"2018-10-12T00:00:00.000Z", :obv=>-54491620}, + {:date_time=>"2018-10-11T00:00:00.000Z", :obv=>-93986390}, + {:date_time=>"2018-10-10T00:00:00.000Z", :obv=>-41084070}, + {:date_time=>"2018-10-09T00:00:00.000Z", :obv=>0} + ] + + expect(normalized_output).to eq(expected_output) + end + + it "Throws exception if not enough data" do + expect {indicator.calculate([])}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('obv') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('On-balance Volume') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq([]) + end + + it 'Validates options' do + valid_options = {} + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = {} + expect(indicator.min_data_size(options)).to eq(1) + end + end + end +end diff --git a/spec/technical_analysis/indicators/rsi_spec.rb b/spec/technical_analysis/indicators/rsi_spec.rb new file mode 100644 index 0000000..569f8c5 --- /dev/null +++ b/spec/technical_analysis/indicators/rsi_spec.rb @@ -0,0 +1,105 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "RSI" do + input_data = SpecHelper.get_test_data(:close, date_time_key: :timestep) + indicator = TechnicalAnalysis::Rsi + + describe 'Relative Strength Index' do + it 'Calculates RSI (14 day)' do + output = indicator.calculate(input_data, period: 14, price_key: :close, date_time_key: :timestep) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :rsi=>41.01572095202713}, + {:date_time=>"2019-01-08T00:00:00.000Z", :rsi=>38.100858593859655}, + {:date_time=>"2019-01-07T00:00:00.000Z", :rsi=>34.80538400125879}, + {:date_time=>"2019-01-04T00:00:00.000Z", :rsi=>35.00790948034705}, + {:date_time=>"2019-01-03T00:00:00.000Z", :rsi=>27.835833051062522}, + {:date_time=>"2019-01-02T00:00:00.000Z", :rsi=>37.90003559360193}, + {:date_time=>"2018-12-31T00:00:00.000Z", :rsi=>37.66054013673465}, + {:date_time=>"2018-12-28T00:00:00.000Z", :rsi=>35.7297472286003}, + {:date_time=>"2018-12-27T00:00:00.000Z", :rsi=>35.63166885267985}, + {:date_time=>"2018-12-26T00:00:00.000Z", :rsi=>36.28727515078207}, + {:date_time=>"2018-12-24T00:00:00.000Z", :rsi=>22.94077952430888}, + {:date_time=>"2018-12-21T00:00:00.000Z", :rsi=>24.757134961511937}, + {:date_time=>"2018-12-20T00:00:00.000Z", :rsi=>27.973957664061984}, + {:date_time=>"2018-12-19T00:00:00.000Z", :rsi=>30.416532991054595}, + {:date_time=>"2018-12-18T00:00:00.000Z", :rsi=>33.926041361905774}, + {:date_time=>"2018-12-17T00:00:00.000Z", :rsi=>30.880956115136144}, + {:date_time=>"2018-12-14T00:00:00.000Z", :rsi=>31.866930881658718}, + {:date_time=>"2018-12-13T00:00:00.000Z", :rsi=>35.61772436774288}, + {:date_time=>"2018-12-12T00:00:00.000Z", :rsi=>33.146531878947414}, + {:date_time=>"2018-12-11T00:00:00.000Z", :rsi=>32.53565170863392}, + {:date_time=>"2018-12-10T00:00:00.000Z", :rsi=>33.115551932880564}, + {:date_time=>"2018-12-07T00:00:00.000Z", :rsi=>31.82436379692355}, + {:date_time=>"2018-12-06T00:00:00.000Z", :rsi=>35.38441991619841}, + {:date_time=>"2018-12-04T00:00:00.000Z", :rsi=>36.58615110748291}, + {:date_time=>"2018-12-03T00:00:00.000Z", :rsi=>42.06016450933706}, + {:date_time=>"2018-11-30T00:00:00.000Z", :rsi=>35.1442970032345}, + {:date_time=>"2018-11-29T00:00:00.000Z", :rsi=>35.760430191057694}, + {:date_time=>"2018-11-28T00:00:00.000Z", :rsi=>36.61457624117541}, + {:date_time=>"2018-11-27T00:00:00.000Z", :rsi=>29.027101402280678}, + {:date_time=>"2018-11-26T00:00:00.000Z", :rsi=>29.211255846774762}, + {:date_time=>"2018-11-23T00:00:00.000Z", :rsi=>26.558433878585916}, + {:date_time=>"2018-11-21T00:00:00.000Z", :rsi=>28.467396310763007}, + {:date_time=>"2018-11-20T00:00:00.000Z", :rsi=>28.552282046348225}, + {:date_time=>"2018-11-19T00:00:00.000Z", :rsi=>32.554453236286506}, + {:date_time=>"2018-11-16T00:00:00.000Z", :rsi=>36.677863857193564}, + {:date_time=>"2018-11-15T00:00:00.000Z", :rsi=>34.550163030510646}, + {:date_time=>"2018-11-14T00:00:00.000Z", :rsi=>29.78632377839773}, + {:date_time=>"2018-11-13T00:00:00.000Z", :rsi=>32.36268849740294}, + {:date_time=>"2018-11-12T00:00:00.000Z", :rsi=>33.31877385045367}, + {:date_time=>"2018-11-09T00:00:00.000Z", :rsi=>38.99885805229995}, + {:date_time=>"2018-11-08T00:00:00.000Z", :rsi=>41.56699725744853}, + {:date_time=>"2018-11-07T00:00:00.000Z", :rsi=>42.51108193242363}, + {:date_time=>"2018-11-06T00:00:00.000Z", :rsi=>36.875893042068746}, + {:date_time=>"2018-11-05T00:00:00.000Z", :rsi=>34.78189708111985}, + {:date_time=>"2018-11-02T00:00:00.000Z", :rsi=>37.93941116682432}, + {:date_time=>"2018-11-01T00:00:00.000Z", :rsi=>48.08268788472883}, + {:date_time=>"2018-10-31T00:00:00.000Z", :rsi=>44.96841382331871}, + {:date_time=>"2018-10-30T00:00:00.000Z", :rsi=>39.38109368376431}, + {:date_time=>"2018-10-29T00:00:00.000Z", :rsi=>38.27160493827161} + ] + + 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+2, price_key: :close, date_time_key: :time)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('rsi') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Relative Strength Index') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period price_key date_time_key)) + end + + it 'Validates options' do + valid_options = { period: 22, price_key: :close, date_time_key: :timespec } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(5) + end + end + end +end diff --git a/spec/technical_analysis/indicators/sma_spec.rb b/spec/technical_analysis/indicators/sma_spec.rb index 5c8dd29..5cf6c08 100644 --- a/spec/technical_analysis/indicators/sma_spec.rb +++ b/spec/technical_analysis/indicators/sma_spec.rb @@ -1,49 +1,114 @@ require 'technical-analysis' +require 'spec_helper' describe 'Indicators' do describe "SMA" do - input_data = { - "2018-12-31": 157.74, - "2018-12-28": 156.23, - "2018-12-27": 156.15, - "2018-12-26": 157.17, - "2018-12-24": 146.83, - "2018-12-21": 150.73, - "2018-12-20": 156.83, - "2018-12-19": 160.89, - "2018-12-18": 166.07, - "2018-12-17": 163.94, - "2018-12-14": 165.48, - "2018-12-13": 170.95, - "2018-12-12": 169.1, - "2018-12-11": 168.63, - "2018-12-10": 169.6 - } + input_data = SpecHelper.get_test_data(:close, date_time_key: :timestep) + indicator = TechnicalAnalysis::Sma describe 'Simple Moving Average' do it 'Calculates SMA (5 day)' do - output = TechnicalAnalysis::Sma.calculate(input_data, period: 5) + output = indicator.calculate(input_data, period: 5, price_key: :close, date_time_key: :timestep) + normalized_output = output.map(&:to_hash) - expected_output = { - :"2018-12-14"=>168.752, - :"2018-12-17"=>167.61999999999998, - :"2018-12-18"=>167.108, - :"2018-12-19"=>165.46599999999998, - :"2018-12-20"=>162.642, - :"2018-12-21"=>159.692, - :"2018-12-24"=>156.27, - :"2018-12-26"=>154.49, - :"2018-12-27"=>153.54199999999997, - :"2018-12-28"=>153.422, - :"2018-12-31"=>154.824 - } + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :sma=>148.488}, + {:date_time=>"2019-01-08T00:00:00.000Z", :sma=>149.41}, + {:date_time=>"2019-01-07T00:00:00.000Z", :sma=>150.808}, + {:date_time=>"2019-01-04T00:00:00.000Z", :sma=>152.468}, + {:date_time=>"2019-01-03T00:00:00.000Z", :sma=>154.046}, + {:date_time=>"2019-01-02T00:00:00.000Z", :sma=>157.04199999999997}, + {:date_time=>"2018-12-31T00:00:00.000Z", :sma=>154.824}, + {:date_time=>"2018-12-28T00:00:00.000Z", :sma=>153.422}, + {:date_time=>"2018-12-27T00:00:00.000Z", :sma=>153.54199999999997}, + {:date_time=>"2018-12-26T00:00:00.000Z", :sma=>154.49}, + {:date_time=>"2018-12-24T00:00:00.000Z", :sma=>156.27}, + {:date_time=>"2018-12-21T00:00:00.000Z", :sma=>159.692}, + {:date_time=>"2018-12-20T00:00:00.000Z", :sma=>162.642}, + {:date_time=>"2018-12-19T00:00:00.000Z", :sma=>165.46599999999998}, + {:date_time=>"2018-12-18T00:00:00.000Z", :sma=>167.108}, + {:date_time=>"2018-12-17T00:00:00.000Z", :sma=>167.61999999999998}, + {:date_time=>"2018-12-14T00:00:00.000Z", :sma=>168.752}, + {:date_time=>"2018-12-13T00:00:00.000Z", :sma=>169.35399999999998}, + {:date_time=>"2018-12-12T00:00:00.000Z", :sma=>170.108}, + {:date_time=>"2018-12-11T00:00:00.000Z", :sma=>171.626}, + {:date_time=>"2018-12-10T00:00:00.000Z", :sma=>174.864}, + {:date_time=>"2018-12-07T00:00:00.000Z", :sma=>176.66}, + {:date_time=>"2018-12-06T00:00:00.000Z", :sma=>178.872}, + {:date_time=>"2018-12-04T00:00:00.000Z", :sma=>180.11600000000004}, + {:date_time=>"2018-12-03T00:00:00.000Z", :sma=>179.62600000000003}, + {:date_time=>"2018-11-30T00:00:00.000Z", :sma=>177.58599999999998}, + {:date_time=>"2018-11-29T00:00:00.000Z", :sma=>176.32799999999997}, + {:date_time=>"2018-11-28T00:00:00.000Z", :sma=>175.77400000000003}, + {:date_time=>"2018-11-27T00:00:00.000Z", :sma=>174.982}, + {:date_time=>"2018-11-26T00:00:00.000Z", :sma=>177.30599999999998}, + {:date_time=>"2018-11-23T00:00:00.000Z", :sma=>181.088}, + {:date_time=>"2018-11-21T00:00:00.000Z", :sma=>184.91199999999998}, + {:date_time=>"2018-11-20T00:00:00.000Z", :sma=>186.916}, + {:date_time=>"2018-11-19T00:00:00.000Z", :sma=>189.96599999999998}, + {:date_time=>"2018-11-16T00:00:00.000Z", :sma=>191.628}, + {:date_time=>"2018-11-15T00:00:00.000Z", :sma=>193.816}, + {:date_time=>"2018-11-14T00:00:00.000Z", :sma=>197.23200000000003}, + {:date_time=>"2018-11-13T00:00:00.000Z", :sma=>201.862}, + {:date_time=>"2018-11-12T00:00:00.000Z", :sma=>204.17000000000002}, + {:date_time=>"2018-11-09T00:00:00.000Z", :sma=>205.654}, + {:date_time=>"2018-11-08T00:00:00.000Z", :sma=>206.256}, + {:date_time=>"2018-11-07T00:00:00.000Z", :sma=>209.002}, + {:date_time=>"2018-11-06T00:00:00.000Z", :sma=>210.78400000000002}, + {:date_time=>"2018-11-05T00:00:00.000Z", :sma=>212.69}, + {:date_time=>"2018-11-02T00:00:00.000Z", :sma=>214.82000000000002}, + {:date_time=>"2018-11-01T00:00:00.000Z", :sma=>216.584}, + {:date_time=>"2018-10-31T00:00:00.000Z", :sma=>216.1}, + {:date_time=>"2018-10-30T00:00:00.000Z", :sma=>215.346}, + {:date_time=>"2018-10-29T00:00:00.000Z", :sma=>217.23200000000003}, + {:date_time=>"2018-10-26T00:00:00.000Z", :sma=>218.914}, + {:date_time=>"2018-10-25T00:00:00.000Z", :sma=>219.51600000000002}, + {:date_time=>"2018-10-24T00:00:00.000Z", :sma=>218.76}, + {:date_time=>"2018-10-23T00:00:00.000Z", :sma=>219.97999999999996}, + {:date_time=>"2018-10-22T00:00:00.000Z", :sma=>219.86400000000003}, + {:date_time=>"2018-10-19T00:00:00.000Z", :sma=>219.206}, + {:date_time=>"2018-10-18T00:00:00.000Z", :sma=>219.766}, + {:date_time=>"2018-10-17T00:00:00.000Z", :sma=>219.452}, + {:date_time=>"2018-10-16T00:00:00.000Z", :sma=>218.48600000000002}, + {:date_time=>"2018-10-15T00:00:00.000Z", :sma=>219.43} + ] - expect(output).to eq(expected_output) + expect(normalized_output).to eq(expected_output) end it "Throws exception if not enough data" do - calc = Calculate.new - expect {TechnicalAnalysis::Sma.calculate(input_data, period: 30)}.to raise_exception(Validation::ValidationError) + 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('sma') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Simple Moving Average') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period price_key date_time_key)) + end + + it 'Validates options' do + valid_options = { period: 22, price_key: :close, date_time_key: :timestep } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(4) end end end diff --git a/spec/technical_analysis/indicators/sr_spec.rb b/spec/technical_analysis/indicators/sr_spec.rb new file mode 100644 index 0000000..6621866 --- /dev/null +++ b/spec/technical_analysis/indicators/sr_spec.rb @@ -0,0 +1,104 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "SR" do + input_data = SpecHelper.get_test_data(:high, :low, :close) + indicator = TechnicalAnalysis::Sr + + describe 'Stochastic Oscillator' do + it 'Calculates SR (14 day)' do + output = indicator.calculate(input_data, period: 14, signal_period: 3) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :sr=>44.44007858546172, :sr_signal=>33.739408752366685}, + {:date_time=>"2019-01-08T00:00:00.000Z", :sr=>34.27340383862123, :sr_signal=>26.631612985573174}, + {:date_time=>"2019-01-07T00:00:00.000Z", :sr=>22.50474383301711, :sr_signal=>15.414319829465327}, + {:date_time=>"2019-01-04T00:00:00.000Z", :sr=>23.1166912850812, :sr_signal=>22.449561749124218}, + {:date_time=>"2019-01-03T00:00:00.000Z", :sr=>0.6215243702976702, :sr_signal=>29.04987430254469}, + {:date_time=>"2019-01-02T00:00:00.000Z", :sr=>43.61046959199379, :sr_signal=>41.21118809340517}, + {:date_time=>"2018-12-31T00:00:00.000Z", :sr=>42.917628945342614, :sr_signal=>38.09610922104404}, + {:date_time=>"2018-12-28T00:00:00.000Z", :sr=>37.105465742879105, :sr_signal=>36.300579364484854}, + {:date_time=>"2018-12-27T00:00:00.000Z", :sr=>34.2652329749104, :sr_signal=>24.155555094877986}, + {:date_time=>"2018-12-26T00:00:00.000Z", :sr=>37.531039375665074, :sr_signal=>13.772232369077116}, + {:date_time=>"2018-12-24T00:00:00.000Z", :sr=>0.6703929340585003, :sr_signal=>2.9825336838014445}, + {:date_time=>"2018-12-21T00:00:00.000Z", :sr=>3.1152647975077716, :sr_signal=>5.080152544595593}, + {:date_time=>"2018-12-20T00:00:00.000Z", :sr=>5.161943319838063, :sr_signal=>9.054487961785336}, + {:date_time=>"2018-12-19T00:00:00.000Z", :sr=>6.963249516440942, :sr_signal=>9.149838987845628}, + {:date_time=>"2018-12-18T00:00:00.000Z", :sr=>15.038271049077, :sr_signal=>10.145121695692445}, + {:date_time=>"2018-12-17T00:00:00.000Z", :sr=>5.447996398018944, :sr_signal=>16.886182356334803}, + {:date_time=>"2018-12-14T00:00:00.000Z", :sr=>9.94909763998139, :sr_signal=>23.970384081443694}, + {:date_time=>"2018-12-13T00:00:00.000Z", :sr=>35.26145303100408, :sr_signal=>28.8292457195742}, + {:date_time=>"2018-12-12T00:00:00.000Z", :sr=>26.700601573345605, :sr_signal=>24.711525960000667}, + {:date_time=>"2018-12-11T00:00:00.000Z", :sr=>24.525682554372914, :sr_signal=>16.048800203857994}, + {:date_time=>"2018-12-10T00:00:00.000Z", :sr=>22.908293752283477, :sr_signal=>13.89015200407954}, + {:date_time=>"2018-12-07T00:00:00.000Z", :sr=>0.712424304917594, :sr_signal=>14.928180772069608}, + {:date_time=>"2018-12-06T00:00:00.000Z", :sr=>18.049737955037553, :sr_signal=>32.719433096383845}, + {:date_time=>"2018-12-04T00:00:00.000Z", :sr=>26.022380056253674, :sr_signal=>36.07538954462583}, + {:date_time=>"2018-12-03T00:00:00.000Z", :sr=>54.0861812778603, :sr_signal=>36.063267521212616}, + {:date_time=>"2018-11-30T00:00:00.000Z", :sr=>28.117607299763502, :sr_signal=>26.965799836520265}, + {:date_time=>"2018-11-29T00:00:00.000Z", :sr=>25.986013986014044, :sr_signal=>20.92157984180065}, + {:date_time=>"2018-11-28T00:00:00.000Z", :sr=>26.79377822378325, :sr_signal=>15.905669844455621}, + {:date_time=>"2018-11-27T00:00:00.000Z", :sr=>9.984947315604659, :sr_signal=>7.140989430040055}, + {:date_time=>"2018-11-26T00:00:00.000Z", :sr=>10.938283993978956, :sr_signal=>4.922619471840783}, + {:date_time=>"2018-11-23T00:00:00.000Z", :sr=>0.49973698053655363, :sr_signal=>2.3224159491234997}, + {:date_time=>"2018-11-21T00:00:00.000Z", :sr=>3.329837441006842, :sr_signal=>2.931860503912096}, + {:date_time=>"2018-11-20T00:00:00.000Z", :sr=>3.137673425827104, :sr_signal=>8.775890351328327}, + {:date_time=>"2018-11-19T00:00:00.000Z", :sr=>2.32807064490234, :sr_signal=>12.744181659747374}, + {:date_time=>"2018-11-16T00:00:00.000Z", :sr=>20.86192698325554, :sr_signal=>12.764205325281347}, + {:date_time=>"2018-11-15T00:00:00.000Z", :sr=>15.04254735108424, :sr_signal=>6.651276276015249}, + {:date_time=>"2018-11-14T00:00:00.000Z", :sr=>2.388141641504267, :sr_signal=>2.0532129671343387}, + {:date_time=>"2018-11-13T00:00:00.000Z", :sr=>2.5231398354572394, :sr_signal=>9.31549269113536}, + {:date_time=>"2018-11-12T00:00:00.000Z", :sr=>1.2483574244415094, :sr_signal=>21.674753063199656}, + {:date_time=>"2018-11-09T00:00:00.000Z", :sr=>24.174980813507332, :sr_signal=>36.326426195958085}, + {:date_time=>"2018-11-08T00:00:00.000Z", :sr=>39.60092095165012, :sr_signal=>35.431056536198575}, + {:date_time=>"2018-11-07T00:00:00.000Z", :sr=>45.203376822716805, :sr_signal=>26.605269889997487}, + {:date_time=>"2018-11-06T00:00:00.000Z", :sr=>21.48887183422879, :sr_signal=>15.172229388808171}, + {:date_time=>"2018-11-05T00:00:00.000Z", :sr=>13.123561013046874, :sr_signal=>37.649110405476506}, + {:date_time=>"2018-11-02T00:00:00.000Z", :sr=>10.904255319148854, :sr_signal=>56.740227701017325}, + {:date_time=>"2018-11-01T00:00:00.000Z", :sr=>88.91951488423378, :sr_signal=>66.35428151414929}, + {:date_time=>"2018-10-31T00:00:00.000Z", :sr=>70.39691289966935, :sr_signal=>46.83290323914804}, + {:date_time=>"2018-10-30T00:00:00.000Z", :sr=>39.746416758544726, :sr_signal=>32.241290132123396} + ] + + 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)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('sr') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Stochastic Oscillator') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period signal_period)) + end + + it 'Validates options' do + valid_options = { period: 22, signal_period: 4 } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4, signal_period: 2 } + expect(indicator.min_data_size(options)).to eq(5) + end + end + end +end diff --git a/spec/technical_analysis/indicators/trix_spec.rb b/spec/technical_analysis/indicators/trix_spec.rb new file mode 100644 index 0000000..33298f1 --- /dev/null +++ b/spec/technical_analysis/indicators/trix_spec.rb @@ -0,0 +1,76 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "TRIX" do + input_data = SpecHelper.get_test_data(:close) + indicator = TechnicalAnalysis::Trix + + describe 'Triple Exponential Average' do + it 'Calculates TRIX (15 day)' do + output = indicator.calculate(input_data, period: 15, price_key: :close) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :trix=>-0.007522826289174942}, + {:date_time=>"2019-01-08T00:00:00.000Z", :trix=>-0.007639218329257057}, + {:date_time=>"2019-01-07T00:00:00.000Z", :trix=>-0.007682172922749195}, + {:date_time=>"2019-01-04T00:00:00.000Z", :trix=>-0.00767662212545961}, + {:date_time=>"2019-01-03T00:00:00.000Z", :trix=>-0.007665196848424279}, + {:date_time=>"2019-01-02T00:00:00.000Z", :trix=>-0.007658933006361239}, + {:date_time=>"2018-12-31T00:00:00.000Z", :trix=>-0.007775594696883065}, + {:date_time=>"2018-12-28T00:00:00.000Z", :trix=>-0.007833433859114468}, + {:date_time=>"2018-12-27T00:00:00.000Z", :trix=>-0.007824502562605692}, + {:date_time=>"2018-12-26T00:00:00.000Z", :trix=>-0.00776162437514068}, + {:date_time=>"2018-12-24T00:00:00.000Z", :trix=>-0.007643867247128558}, + {:date_time=>"2018-12-21T00:00:00.000Z", :trix=>-0.007455715388896471}, + {:date_time=>"2018-12-20T00:00:00.000Z", :trix=>-0.00735126292321306}, + {:date_time=>"2018-12-19T00:00:00.000Z", :trix=>-0.007332615495168546}, + {:date_time=>"2018-12-18T00:00:00.000Z", :trix=>-0.007365504921638632}, + {:date_time=>"2018-12-17T00:00:00.000Z", :trix=>-0.007428539823046746}, + {:date_time=>"2018-12-14T00:00:00.000Z", :trix=>-0.007473061875390961}, + {:date_time=>"2018-12-13T00:00:00.000Z", :trix=>-0.007537530463588133}, + {:date_time=>"2018-12-12T00:00:00.000Z", :trix=>-0.007622995995719011}, + {:date_time=>"2018-12-11T00:00:00.000Z", :trix=>-0.007673381215454718} + ] + + 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 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('trix') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Triple Exponential Average') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period price_key)) + end + + it 'Validates options' do + valid_options = { period: 22, price_key: :close } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(11) + end + end + end +end diff --git a/spec/technical_analysis/indicators/tsi_spec.rb b/spec/technical_analysis/indicators/tsi_spec.rb new file mode 100644 index 0000000..4151ae9 --- /dev/null +++ b/spec/technical_analysis/indicators/tsi_spec.rb @@ -0,0 +1,87 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "TSI" do + input_data = SpecHelper.get_test_data(:close) + indicator = TechnicalAnalysis::Tsi + + describe 'True Strength Index' do + it 'Calculates True Strength Index' do + output = indicator.calculate(input_data, low_period: 13, high_period: 25, price_key: :close) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :tsi=>-28.91017661103889}, + {:date_time=>"2019-01-08T00:00:00.000Z", :tsi=>-30.97413963420104}, + {:date_time=>"2019-01-07T00:00:00.000Z", :tsi=>-32.39480993311267}, + {:date_time=>"2019-01-04T00:00:00.000Z", :tsi=>-32.874679857827935}, + {:date_time=>"2019-01-03T00:00:00.000Z", :tsi=>-33.579027007940994}, + {:date_time=>"2019-01-02T00:00:00.000Z", :tsi=>-31.495178028566524}, + {:date_time=>"2018-12-31T00:00:00.000Z", :tsi=>-32.785927300024994}, + {:date_time=>"2018-12-28T00:00:00.000Z", :tsi=>-34.28080772951784}, + {:date_time=>"2018-12-27T00:00:00.000Z", :tsi=>-35.41195667338275}, + {:date_time=>"2018-12-26T00:00:00.000Z", :tsi=>-36.802531445786066}, + {:date_time=>"2018-12-24T00:00:00.000Z", :tsi=>-38.83883734905748}, + {:date_time=>"2018-12-21T00:00:00.000Z", :tsi=>-36.202827241203856}, + {:date_time=>"2018-12-20T00:00:00.000Z", :tsi=>-33.78946079860395}, + {:date_time=>"2018-12-19T00:00:00.000Z", :tsi=>-32.23640227177938}, + {:date_time=>"2018-12-18T00:00:00.000Z", :tsi=>-31.30170501401141}, + {:date_time=>"2018-12-17T00:00:00.000Z", :tsi=>-31.403055885745307}, + {:date_time=>"2018-12-14T00:00:00.000Z", :tsi=>-30.569488217596724}, + {:date_time=>"2018-12-13T00:00:00.000Z", :tsi=>-29.91401235092671}, + {:date_time=>"2018-12-12T00:00:00.000Z", :tsi=>-30.375476807689427}, + {:date_time=>"2018-12-11T00:00:00.000Z", :tsi=>-30.15935663446155}, + {:date_time=>"2018-12-10T00:00:00.000Z", :tsi=>-29.704744795960114}, + {:date_time=>"2018-12-07T00:00:00.000Z", :tsi=>-29.357900955250976}, + {:date_time=>"2018-12-06T00:00:00.000Z", :tsi=>-28.4796578607378}, + {:date_time=>"2018-12-04T00:00:00.000Z", :tsi=>-28.752945178257534}, + {:date_time=>"2018-12-03T00:00:00.000Z", :tsi=>-29.524570107700253}, + {:date_time=>"2018-11-30T00:00:00.000Z", :tsi=>-32.212798598181024} + ] + + expect(normalized_output).to eq(expected_output) + end + + it "Throws exception if not enough data" do + low_period = 13 + high_period = 25 + size_limit = low_period + high_period - 1 + input_data = input_data.first(size_limit) + + expect {indicator.calculate(input_data, low_period: low_period, high_period: high_period, 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('tsi') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('True Strength Index') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(low_period high_period price_key)) + end + + it 'Validates options' do + valid_options = { low_period: 10, high_period: 20, price_key: :close } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { low_period: 10, high_period: 20 } + expect(indicator.min_data_size(options)).to eq(30) + end + end + end +end diff --git a/spec/technical_analysis/indicators/uo_spec.rb b/spec/technical_analysis/indicators/uo_spec.rb new file mode 100644 index 0000000..63b8037 --- /dev/null +++ b/spec/technical_analysis/indicators/uo_spec.rb @@ -0,0 +1,91 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "UO" do + input_data = SpecHelper.get_test_data(:high, :low, :close) + indicator = TechnicalAnalysis::Uo + + describe 'Ultimate Oscillator' do + it 'Calculates UO (5 day)' do + output = indicator.calculate(input_data, short_period: 7, medium_period: 14, long_period: 28, short_weight: 4, medium_weight: 2, long_weight: 1) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :uo=>47.28872762629681}, + {:date_time=>"2019-01-08T00:00:00.000Z", :uo=>44.828908983561035}, + {:date_time=>"2019-01-07T00:00:00.000Z", :uo=>46.58165158841807}, + {:date_time=>"2019-01-04T00:00:00.000Z", :uo=>50.726610056055335}, + {:date_time=>"2019-01-03T00:00:00.000Z", :uo=>43.660461129633255}, + {:date_time=>"2019-01-02T00:00:00.000Z", :uo=>51.63477005783912}, + {:date_time=>"2018-12-31T00:00:00.000Z", :uo=>46.12625788315007}, + {:date_time=>"2018-12-28T00:00:00.000Z", :uo=>44.736408028234784}, + {:date_time=>"2018-12-27T00:00:00.000Z", :uo=>44.80062908207362}, + {:date_time=>"2018-12-26T00:00:00.000Z", :uo=>38.89197235556109}, + {:date_time=>"2018-12-24T00:00:00.000Z", :uo=>24.01487703769969}, + {:date_time=>"2018-12-21T00:00:00.000Z", :uo=>28.31825074884628}, + {:date_time=>"2018-12-20T00:00:00.000Z", :uo=>30.219780692869403}, + {:date_time=>"2018-12-19T00:00:00.000Z", :uo=>31.693410049073588}, + {:date_time=>"2018-12-18T00:00:00.000Z", :uo=>42.6017793901735}, + {:date_time=>"2018-12-17T00:00:00.000Z", :uo=>38.091246151228205}, + {:date_time=>"2018-12-14T00:00:00.000Z", :uo=>43.99139965247388}, + {:date_time=>"2018-12-13T00:00:00.000Z", :uo=>42.5926187322538}, + {:date_time=>"2018-12-12T00:00:00.000Z", :uo=>46.56343334880161}, + {:date_time=>"2018-12-11T00:00:00.000Z", :uo=>47.39018769748099}, + {:date_time=>"2018-12-10T00:00:00.000Z", :uo=>46.78620949538559}, + {:date_time=>"2018-12-07T00:00:00.000Z", :uo=>46.3795568325064}, + {:date_time=>"2018-12-06T00:00:00.000Z", :uo=>54.531561679403225}, + {:date_time=>"2018-12-04T00:00:00.000Z", :uo=>54.53131218526129}, + {:date_time=>"2018-12-03T00:00:00.000Z", :uo=>58.63742128493122}, + {:date_time=>"2018-11-30T00:00:00.000Z", :uo=>48.76091038333585}, + {:date_time=>"2018-11-29T00:00:00.000Z", :uo=>42.75825214517756}, + {:date_time=>"2018-11-28T00:00:00.000Z", :uo=>39.6100967511459}, + {:date_time=>"2018-11-27T00:00:00.000Z", :uo=>36.448196445521205}, + {:date_time=>"2018-11-26T00:00:00.000Z", :uo=>37.129780535443615}, + {:date_time=>"2018-11-23T00:00:00.000Z", :uo=>30.31628731487427}, + {:date_time=>"2018-11-21T00:00:00.000Z", :uo=>30.12209085444982}, + {:date_time=>"2018-11-20T00:00:00.000Z", :uo=>29.938582222468735}, + {:date_time=>"2018-11-19T00:00:00.000Z", :uo=>33.50557345611348}, + {:date_time=>"2018-11-16T00:00:00.000Z", :uo=>37.48593642184523} + ] + + expect(normalized_output).to eq(expected_output) + end + + it "Throws exception if not enough data" do + expect {indicator.calculate(input_data, long_period: input_data.size+2)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('uo') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Ultimate Oscillator') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(short_period medium_period long_period short_weight medium_weight long_weight)) + end + + it 'Validates options' do + valid_options = { short_period: 7, medium_period: 14, long_period: 28, short_weight: 4, medium_weight: 2, long_weight: 1 } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { short_period: 7, medium_period: 14, long_period: 20, short_weight: 4, medium_weight: 2, long_weight: 1 } + expect(indicator.min_data_size(options)).to eq(21) + end + end + end +end diff --git a/spec/technical_analysis/indicators/vi_spec.rb b/spec/technical_analysis/indicators/vi_spec.rb new file mode 100644 index 0000000..d85f318 --- /dev/null +++ b/spec/technical_analysis/indicators/vi_spec.rb @@ -0,0 +1,105 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "VI" do + input_data = SpecHelper.get_test_data(:high, :low, :close) + indicator = TechnicalAnalysis::Vi + + describe 'Vortex Indicator' do + it 'Calculates VI (14 day)' do + output = indicator.calculate(input_data, period: 14) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :negative_vi=>0.9777149447928525, :positive_vi=>0.8609629970246735}, + {:date_time=>"2019-01-08T00:00:00.000Z", :negative_vi=>1.0113586362578701, :positive_vi=>0.8600571901821686}, + {:date_time=>"2019-01-07T00:00:00.000Z", :negative_vi=>1.0577860164333046, :positive_vi=>0.813115261460082}, + {:date_time=>"2019-01-04T00:00:00.000Z", :negative_vi=>1.0760915145470467, :positive_vi=>0.7417758715458455}, + {:date_time=>"2019-01-03T00:00:00.000Z", :negative_vi=>1.114675915889877, :positive_vi=>0.7324951224799482}, + {:date_time=>"2019-01-02T00:00:00.000Z", :negative_vi=>1.1146552806731134, :positive_vi=>0.8035916112018086}, + {:date_time=>"2018-12-31T00:00:00.000Z", :negative_vi=>1.08671679197995, :positive_vi=>0.8781954887218045}, + {:date_time=>"2018-12-28T00:00:00.000Z", :negative_vi=>1.1487474529545731, :positive_vi=>0.8037876063766033}, + {:date_time=>"2018-12-27T00:00:00.000Z", :negative_vi=>1.1655798789007923, :positive_vi=>0.729855612482534}, + {:date_time=>"2018-12-26T00:00:00.000Z", :negative_vi=>1.2335085243974138, :positive_vi=>0.636331569664903}, + {:date_time=>"2018-12-24T00:00:00.000Z", :negative_vi=>1.3088205560235893, :positive_vi=>0.5374882657359492}, + {:date_time=>"2018-12-21T00:00:00.000Z", :negative_vi=>1.1606095395904847, :positive_vi=>0.5994780447390226}, + {:date_time=>"2018-12-20T00:00:00.000Z", :negative_vi=>1.1525346959374216, :positive_vi=>0.6361329800656076}, + {:date_time=>"2018-12-19T00:00:00.000Z", :negative_vi=>1.067568020631851, :positive_vi=>0.7127001934235981}, + {:date_time=>"2018-12-18T00:00:00.000Z", :negative_vi=>0.9773071878279123, :positive_vi=>0.8213523084994758}, + {:date_time=>"2018-12-17T00:00:00.000Z", :negative_vi=>0.974913770577476, :positive_vi=>0.814344133786256}, + {:date_time=>"2018-12-14T00:00:00.000Z", :negative_vi=>0.9867067848168232, :positive_vi=>0.8214508662875287}, + {:date_time=>"2018-12-13T00:00:00.000Z", :negative_vi=>1.011590726346824, :positive_vi=>0.8266537121415173}, + {:date_time=>"2018-12-12T00:00:00.000Z", :negative_vi=>1.0422719380259118, :positive_vi=>0.8400547615867511}, + {:date_time=>"2018-12-11T00:00:00.000Z", :negative_vi=>1.1040510191627, :positive_vi=>0.7505785426583675}, + {:date_time=>"2018-12-10T00:00:00.000Z", :negative_vi=>1.136139122315593, :positive_vi=>0.6334605508870221}, + {:date_time=>"2018-12-07T00:00:00.000Z", :negative_vi=>1.050514334444714, :positive_vi=>0.7172185077490695}, + {:date_time=>"2018-12-06T00:00:00.000Z", :negative_vi=>1.0795256042654737, :positive_vi=>0.7516804020221332}, + {:date_time=>"2018-12-04T00:00:00.000Z", :negative_vi=>1.0423007389465182, :positive_vi=>0.7496451535522679}, + {:date_time=>"2018-12-03T00:00:00.000Z", :negative_vi=>1.0746012192731307, :positive_vi=>0.8019709726837323}, + {:date_time=>"2018-11-30T00:00:00.000Z", :negative_vi=>1.1516224812958684, :positive_vi=>0.6987674707967169}, + {:date_time=>"2018-11-29T00:00:00.000Z", :negative_vi=>1.1373201600900555, :positive_vi=>0.6548920237509929}, + {:date_time=>"2018-11-28T00:00:00.000Z", :negative_vi=>1.1564601777941095, :positive_vi=>0.6464192792510783}, + {:date_time=>"2018-11-27T00:00:00.000Z", :negative_vi=>1.1714346511931206, :positive_vi=>0.6287012609627801}, + {:date_time=>"2018-11-26T00:00:00.000Z", :negative_vi=>1.1658984366885399, :positive_vi=>0.65775873808705}, + {:date_time=>"2018-11-23T00:00:00.000Z", :negative_vi=>1.2070792620527797, :positive_vi=>0.6046320015251558}, + {:date_time=>"2018-11-21T00:00:00.000Z", :negative_vi=>1.1504168141815485, :positive_vi=>0.5639363354788292}, + {:date_time=>"2018-11-20T00:00:00.000Z", :negative_vi=>1.1182403853648055, :positive_vi=>0.5634420498548615}, + {:date_time=>"2018-11-19T00:00:00.000Z", :negative_vi=>1.018527704309603, :positive_vi=>0.6562081533662589}, + {:date_time=>"2018-11-16T00:00:00.000Z", :negative_vi=>1.0499147710692558, :positive_vi=>0.7525644147579889}, + {:date_time=>"2018-11-15T00:00:00.000Z", :negative_vi=>1.0787197159390678, :positive_vi=>0.6861446786495575}, + {:date_time=>"2018-11-14T00:00:00.000Z", :negative_vi=>1.0659261208578774, :positive_vi=>0.6477869675714517}, + {:date_time=>"2018-11-13T00:00:00.000Z", :negative_vi=>1.053272641569953, :positive_vi=>0.6968210271671884}, + {:date_time=>"2018-11-12T00:00:00.000Z", :negative_vi=>1.0186513629842182, :positive_vi=>0.7271341463414641}, + {:date_time=>"2018-11-09T00:00:00.000Z", :negative_vi=>1.0059420422342082, :positive_vi=>0.7587530852911607}, + {:date_time=>"2018-11-08T00:00:00.000Z", :negative_vi=>0.9713674816398625, :positive_vi=>0.8198382448638102}, + {:date_time=>"2018-11-07T00:00:00.000Z", :negative_vi=>0.9456323099415208, :positive_vi=>0.8265716374269011}, + {:date_time=>"2018-11-06T00:00:00.000Z", :negative_vi=>1.0107777977366625, :positive_vi=>0.7408837794144069}, + {:date_time=>"2018-11-05T00:00:00.000Z", :negative_vi=>1.017756255044391, :positive_vi=>0.73372163931486}, + {:date_time=>"2018-11-02T00:00:00.000Z", :negative_vi=>0.9506581829483907, :positive_vi=>0.7977446639361122}, + {:date_time=>"2018-11-01T00:00:00.000Z", :negative_vi=>0.9510765744895432, :positive_vi=>0.9155241699342749}, + {:date_time=>"2018-10-31T00:00:00.000Z", :negative_vi=>0.9136449963918103, :positive_vi=>0.9374983015842824}, + {:date_time=>"2018-10-30T00:00:00.000Z", :negative_vi=>1.0400453579079016, :positive_vi=>0.8594387814137219}, + {:date_time=>"2018-10-29T00:00:00.000Z", :negative_vi=>0.9987535631315481, :positive_vi=>0.7711714493088508} + ] + + 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)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('vi') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Vortex Indicator') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period)) + end + + it 'Validates options' do + valid_options = { period: 22 } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(5) + end + end + end +end diff --git a/spec/technical_analysis/indicators/vpt_spec.rb b/spec/technical_analysis/indicators/vpt_spec.rb new file mode 100644 index 0000000..6ea35e0 --- /dev/null +++ b/spec/technical_analysis/indicators/vpt_spec.rb @@ -0,0 +1,118 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "VPT" do + input_data = SpecHelper.get_test_data(:close, :volume) + indicator = TechnicalAnalysis::Vpt + + describe 'Volume-Price Trend' do + it 'Calculates VPT' do + output = indicator.calculate(input_data) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :vpt=>-27383899.78109331}, + {:date_time=>"2019-01-08T00:00:00.000Z", :vpt=>-28148662.548589166}, + {:date_time=>"2019-01-07T00:00:00.000Z", :vpt=>-28923059.940598898}, + {:date_time=>"2019-01-04T00:00:00.000Z", :vpt=>-28801593.76496151}, + {:date_time=>"2019-01-03T00:00:00.000Z", :vpt=>-31252972.592586517}, + {:date_time=>"2019-01-02T00:00:00.000Z", :vpt=>-22178057.48873647}, + {:date_time=>"2018-12-31T00:00:00.000Z", :vpt=>-22218723.601326805}, + {:date_time=>"2018-12-28T00:00:00.000Z", :vpt=>-22552168.387219403}, + {:date_time=>"2018-12-27T00:00:00.000Z", :vpt=>-22573553.26073845}, + {:date_time=>"2018-12-26T00:00:00.000Z", :vpt=>-22238622.758734256}, + {:date_time=>"2018-12-24T00:00:00.000Z", :vpt=>-26332500.093066465}, + {:date_time=>"2018-12-21T00:00:00.000Z", :vpt=>-25370780.481841102}, + {:date_time=>"2018-12-20T00:00:00.000Z", :vpt=>-21656330.504158247}, + {:date_time=>"2018-12-19T00:00:00.000Z", :vpt=>-20031264.8456338}, + {:date_time=>"2018-12-18T00:00:00.000Z", :vpt=>-18546614.21276814}, + {:date_time=>"2018-12-17T00:00:00.000Z", :vpt=>-18985158.397835847}, + {:date_time=>"2018-12-14T00:00:00.000Z", :vpt=>-18582658.71932485}, + {:date_time=>"2018-12-13T00:00:00.000Z", :vpt=>-17282902.245502096}, + {:date_time=>"2018-12-12T00:00:00.000Z", :vpt=>-17630301.940948576}, + {:date_time=>"2018-12-11T00:00:00.000Z", :vpt=>-17729175.804436687}, + {:date_time=>"2018-12-10T00:00:00.000Z", :vpt=>-17466268.97188952}, + {:date_time=>"2018-12-07T00:00:00.000Z", :vpt=>-17873132.82137613}, + {:date_time=>"2018-12-06T00:00:00.000Z", :vpt=>-16386993.991247926}, + {:date_time=>"2018-12-04T00:00:00.000Z", :vpt=>-15910856.843135413}, + {:date_time=>"2018-12-03T00:00:00.000Z", :vpt=>-14101104.854714246}, + {:date_time=>"2018-11-30T00:00:00.000Z", :vpt=>-15517586.252407152}, + {:date_time=>"2018-11-29T00:00:00.000Z", :vpt=>-15304600.832189944}, + {:date_time=>"2018-11-28T00:00:00.000Z", :vpt=>-14985612.348714761}, + {:date_time=>"2018-11-27T00:00:00.000Z", :vpt=>-16752197.088154612}, + {:date_time=>"2018-11-26T00:00:00.000Z", :vpt=>-16662634.99217477}, + {:date_time=>"2018-11-23T00:00:00.000Z", :vpt=>-17266635.256844807}, + {:date_time=>"2018-11-21T00:00:00.000Z", :vpt=>-16666614.749434467}, + {:date_time=>"2018-11-20T00:00:00.000Z", :vpt=>-16631473.78435367}, + {:date_time=>"2018-11-19T00:00:00.000Z", :vpt=>-13397928.75906581}, + {:date_time=>"2018-11-16T00:00:00.000Z", :vpt=>-11748170.533467716}, + {:date_time=>"2018-11-15T00:00:00.000Z", :vpt=>-12149014.896876106}, + {:date_time=>"2018-11-14T00:00:00.000Z", :vpt=>-13290943.979317216}, + {:date_time=>"2018-11-13T00:00:00.000Z", :vpt=>-11580638.32359231}, + {:date_time=>"2018-11-12T00:00:00.000Z", :vpt=>-11113790.317206156}, + {:date_time=>"2018-11-09T00:00:00.000Z", :vpt=>-8545161.134440955}, + {:date_time=>"2018-11-08T00:00:00.000Z", :vpt=>-7883463.234301859}, + {:date_time=>"2018-11-07T00:00:00.000Z", :vpt=>-7707600.723227796}, + {:date_time=>"2018-11-06T00:00:00.000Z", :vpt=>-8717279.945880784}, + {:date_time=>"2018-11-05T00:00:00.000Z", :vpt=>-9060892.67270255}, + {:date_time=>"2018-11-02T00:00:00.000Z", :vpt=>-7185217.517024899}, + {:date_time=>"2018-11-01T00:00:00.000Z", :vpt=>-1146038.8004377298}, + {:date_time=>"2018-10-31T00:00:00.000Z", :vpt=>-1959004.510023763}, + {:date_time=>"2018-10-30T00:00:00.000Z", :vpt=>-2949972.4593908517}, + {:date_time=>"2018-10-29T00:00:00.000Z", :vpt=>-3132205.8074873467}, + {:date_time=>"2018-10-26T00:00:00.000Z", :vpt=>-2274149.490335243}, + {:date_time=>"2018-10-25T00:00:00.000Z", :vpt=>-1522689.2992524402}, + {:date_time=>"2018-10-24T00:00:00.000Z", :vpt=>-2158324.481734193}, + {:date_time=>"2018-10-23T00:00:00.000Z", :vpt=>-786529.9466468699}, + {:date_time=>"2018-10-22T00:00:00.000Z", :vpt=>-1151165.4943468445}, + {:date_time=>"2018-10-19T00:00:00.000Z", :vpt=>-1326839.4882367724}, + {:date_time=>"2018-10-18T00:00:00.000Z", :vpt=>-1827517.8777377433}, + {:date_time=>"2018-10-17T00:00:00.000Z", :vpt=>-1070464.7668376141}, + {:date_time=>"2018-10-16T00:00:00.000Z", :vpt=>-972399.6540759658}, + {:date_time=>"2018-10-15T00:00:00.000Z", :vpt=>-1607126.441433344}, + {:date_time=>"2018-10-12T00:00:00.000Z", :vpt=>-959554.7990039173}, + {:date_time=>"2018-10-11T00:00:00.000Z", :vpt=>-2370279.621573285}, + {:date_time=>"2018-10-10T00:00:00.000Z", :vpt=>-1903264.3174505206} + ] + + expect(normalized_output).to eq(expected_output) + end + + it "Throws exception if not enough data" do + expect {indicator.calculate([])}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('vpt') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Volume-price Trend') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq([]) + end + + it 'Validates options' do + valid_options = {} + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = {} + expect(indicator.min_data_size(options)).to eq(2) + end + end + end +end diff --git a/spec/technical_analysis/indicators/vwap_spec.rb b/spec/technical_analysis/indicators/vwap_spec.rb new file mode 100644 index 0000000..73e834b --- /dev/null +++ b/spec/technical_analysis/indicators/vwap_spec.rb @@ -0,0 +1,119 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "VWAP" do + input_data = SpecHelper.get_test_data(:volume, :high, :low, :close) + indicator = TechnicalAnalysis::Vwap + + describe 'Volume Weighted Average Price' do + it 'Calculates VWAP' do + output = indicator.calculate(input_data) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :vwap=>183.55426863053765}, + {:date_time=>"2019-01-08T00:00:00.000Z", :vwap=>184.06635227887782}, + {:date_time=>"2019-01-07T00:00:00.000Z", :vwap=>184.57507590747517}, + {:date_time=>"2019-01-04T00:00:00.000Z", :vwap=>185.3413093796368}, + {:date_time=>"2019-01-03T00:00:00.000Z", :vwap=>186.19781421865375}, + {:date_time=>"2019-01-02T00:00:00.000Z", :vwap=>187.76843731172002}, + {:date_time=>"2018-12-31T00:00:00.000Z", :vwap=>188.21552447966795}, + {:date_time=>"2018-12-28T00:00:00.000Z", :vwap=>188.64862275471472}, + {:date_time=>"2018-12-27T00:00:00.000Z", :vwap=>189.2144975598256}, + {:date_time=>"2018-12-26T00:00:00.000Z", :vwap=>189.9889456493791}, + {:date_time=>"2018-12-24T00:00:00.000Z", :vwap=>190.91953458787302}, + {:date_time=>"2018-12-21T00:00:00.000Z", :vwap=>191.6297166885398}, + {:date_time=>"2018-12-20T00:00:00.000Z", :vwap=>193.36567531270202}, + {:date_time=>"2018-12-19T00:00:00.000Z", :vwap=>194.463693818502}, + {:date_time=>"2018-12-18T00:00:00.000Z", :vwap=>195.21670632642397}, + {:date_time=>"2018-12-17T00:00:00.000Z", :vwap=>195.7127882800064}, + {:date_time=>"2018-12-14T00:00:00.000Z", :vwap=>196.3956319251192}, + {:date_time=>"2018-12-13T00:00:00.000Z", :vwap=>197.03092567150182}, + {:date_time=>"2018-12-12T00:00:00.000Z", :vwap=>197.47196479514173}, + {:date_time=>"2018-12-11T00:00:00.000Z", :vwap=>198.00221355940155}, + {:date_time=>"2018-12-10T00:00:00.000Z", :vwap=>198.7429667210245}, + {:date_time=>"2018-12-07T00:00:00.000Z", :vwap=>199.85255982037654}, + {:date_time=>"2018-12-06T00:00:00.000Z", :vwap=>200.57927838438474}, + {:date_time=>"2018-12-04T00:00:00.000Z", :vwap=>201.28731920049952}, + {:date_time=>"2018-12-03T00:00:00.000Z", :vwap=>201.8731568533139}, + {:date_time=>"2018-11-30T00:00:00.000Z", :vwap=>202.34554208942396}, + {:date_time=>"2018-11-29T00:00:00.000Z", :vwap=>202.95867828112358}, + {:date_time=>"2018-11-28T00:00:00.000Z", :vwap=>203.60135199869035}, + {:date_time=>"2018-11-27T00:00:00.000Z", :vwap=>204.38651817163168}, + {:date_time=>"2018-11-26T00:00:00.000Z", :vwap=>205.30361785858756}, + {:date_time=>"2018-11-23T00:00:00.000Z", :vwap=>206.36274937523916}, + {:date_time=>"2018-11-21T00:00:00.000Z", :vwap=>206.94494917055295}, + {:date_time=>"2018-11-20T00:00:00.000Z", :vwap=>207.6427516507437}, + {:date_time=>"2018-11-19T00:00:00.000Z", :vwap=>209.27699957144853}, + {:date_time=>"2018-11-16T00:00:00.000Z", :vwap=>210.05211892678065}, + {:date_time=>"2018-11-15T00:00:00.000Z", :vwap=>210.59952347188045}, + {:date_time=>"2018-11-14T00:00:00.000Z", :vwap=>211.4589551886514}, + {:date_time=>"2018-11-13T00:00:00.000Z", :vwap=>212.75803369499064}, + {:date_time=>"2018-11-12T00:00:00.000Z", :vwap=>213.6551619737374}, + {:date_time=>"2018-11-09T00:00:00.000Z", :vwap=>214.61043573066618}, + {:date_time=>"2018-11-08T00:00:00.000Z", :vwap=>215.00076838969952}, + {:date_time=>"2018-11-07T00:00:00.000Z", :vwap=>215.18761540540189}, + {:date_time=>"2018-11-06T00:00:00.000Z", :vwap=>215.4663555747261}, + {:date_time=>"2018-11-05T00:00:00.000Z", :vwap=>215.93354729003553}, + {:date_time=>"2018-11-02T00:00:00.000Z", :vwap=>217.20680955786048}, + {:date_time=>"2018-11-01T00:00:00.000Z", :vwap=>218.35223508156088}, + {:date_time=>"2018-10-31T00:00:00.000Z", :vwap=>218.1692825392702}, + {:date_time=>"2018-10-30T00:00:00.000Z", :vwap=>218.13783195840017}, + {:date_time=>"2018-10-29T00:00:00.000Z", :vwap=>218.5155747290589}, + {:date_time=>"2018-10-26T00:00:00.000Z", :vwap=>219.05970452474796}, + {:date_time=>"2018-10-25T00:00:00.000Z", :vwap=>219.3440526461074}, + {:date_time=>"2018-10-24T00:00:00.000Z", :vwap=>219.34643675779574}, + {:date_time=>"2018-10-23T00:00:00.000Z", :vwap=>219.4951598586493}, + {:date_time=>"2018-10-22T00:00:00.000Z", :vwap=>219.41092910048724}, + {:date_time=>"2018-10-19T00:00:00.000Z", :vwap=>219.26375336145847}, + {:date_time=>"2018-10-18T00:00:00.000Z", :vwap=>219.25541442468506}, + {:date_time=>"2018-10-17T00:00:00.000Z", :vwap=>219.65735634491844}, + {:date_time=>"2018-10-16T00:00:00.000Z", :vwap=>219.5125052055069}, + {:date_time=>"2018-10-15T00:00:00.000Z", :vwap=>219.34283327445593}, + {:date_time=>"2018-10-12T00:00:00.000Z", :vwap=>219.44169580294155}, + {:date_time=>"2018-10-11T00:00:00.000Z", :vwap=>219.0592293697168}, + {:date_time=>"2018-10-10T00:00:00.000Z", :vwap=>221.89869420553177}, + {:date_time=>"2018-10-09T00:00:00.000Z", :vwap=>225.4620666666667} + ] + + expect(normalized_output).to eq(expected_output) + end + + it "Throws exception if not enough data" do + expect {indicator.calculate([])}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('vwap') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Volume Weighted Average Price') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq([]) + end + + it 'Validates options' do + valid_options = {} + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = {} + expect(indicator.min_data_size(options)).to eq(1) + end + end + end +end diff --git a/spec/technical_analysis/indicators/wma_spec.rb b/spec/technical_analysis/indicators/wma_spec.rb new file mode 100644 index 0000000..20d4a87 --- /dev/null +++ b/spec/technical_analysis/indicators/wma_spec.rb @@ -0,0 +1,115 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "WMA" do + input_data = SpecHelper.get_test_data(:close, date_time_key: :timestep) + indicator = TechnicalAnalysis::Wma + + describe 'Weighted Moving Average' do + it 'Calculates WMA (5 day)' do + output = indicator.calculate(input_data, period: 5, price_key: :close, date_time_key: :timestep) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :wma=>150.13666666666666}, + {:date_time=>"2019-01-08T00:00:00.000Z", :wma=>148.83666666666667}, + {:date_time=>"2019-01-07T00:00:00.000Z", :wma=>148.856}, + {:date_time=>"2019-01-04T00:00:00.000Z", :wma=>150.36866666666666}, + {:date_time=>"2019-01-03T00:00:00.000Z", :wma=>152.29733333333334}, + {:date_time=>"2019-01-02T00:00:00.000Z", :wma=>157.248}, + {:date_time=>"2018-12-31T00:00:00.000Z", :wma=>156.216}, + {:date_time=>"2018-12-28T00:00:00.000Z", :wma=>154.77666666666667}, + {:date_time=>"2018-12-27T00:00:00.000Z", :wma=>153.88066666666668}, + {:date_time=>"2018-12-26T00:00:00.000Z", :wma=>153.3273333333333}, + {:date_time=>"2018-12-24T00:00:00.000Z", :wma=>153.02733333333333}, + {:date_time=>"2018-12-21T00:00:00.000Z", :wma=>157.31466666666668}, + {:date_time=>"2018-12-20T00:00:00.000Z", :wma=>161.28533333333334}, + {:date_time=>"2018-12-19T00:00:00.000Z", :wma=>164.164}, + {:date_time=>"2018-12-18T00:00:00.000Z", :wma=>166.23666666666665}, + {:date_time=>"2018-12-17T00:00:00.000Z", :wma=>166.75333333333333}, + {:date_time=>"2018-12-14T00:00:00.000Z", :wma=>168.35733333333334}, + {:date_time=>"2018-12-13T00:00:00.000Z", :wma=>169.64866666666666}, + {:date_time=>"2018-12-12T00:00:00.000Z", :wma=>169.368}, + {:date_time=>"2018-12-11T00:00:00.000Z", :wma=>170.21}, + {:date_time=>"2018-12-10T00:00:00.000Z", :wma=>172.288}, + {:date_time=>"2018-12-07T00:00:00.000Z", :wma=>174.64133333333334}, + {:date_time=>"2018-12-06T00:00:00.000Z", :wma=>178.102}, + {:date_time=>"2018-12-04T00:00:00.000Z", :wma=>179.9006666666667}, + {:date_time=>"2018-12-03T00:00:00.000Z", :wma=>180.87933333333334}, + {:date_time=>"2018-11-30T00:00:00.000Z", :wma=>178.468}, + {:date_time=>"2018-11-29T00:00:00.000Z", :wma=>177.71733333333333}, + {:date_time=>"2018-11-28T00:00:00.000Z", :wma=>176.45866666666666}, + {:date_time=>"2018-11-27T00:00:00.000Z", :wma=>174.47266666666667}, + {:date_time=>"2018-11-26T00:00:00.000Z", :wma=>175.49466666666666}, + {:date_time=>"2018-11-23T00:00:00.000Z", :wma=>177.65066666666667}, + {:date_time=>"2018-11-21T00:00:00.000Z", :wma=>181.858}, + {:date_time=>"2018-11-20T00:00:00.000Z", :wma=>185.23666666666668}, + {:date_time=>"2018-11-19T00:00:00.000Z", :wma=>189.56533333333334}, + {:date_time=>"2018-11-16T00:00:00.000Z", :wma=>191.488}, + {:date_time=>"2018-11-15T00:00:00.000Z", :wma=>191.58333333333331}, + {:date_time=>"2018-11-14T00:00:00.000Z", :wma=>193.524}, + {:date_time=>"2018-11-13T00:00:00.000Z", :wma=>198.54466666666667}, + {:date_time=>"2018-11-12T00:00:00.000Z", :wma=>202.52466666666666}, + {:date_time=>"2018-11-09T00:00:00.000Z", :wma=>206.35266666666666}, + {:date_time=>"2018-11-08T00:00:00.000Z", :wma=>206.948}, + {:date_time=>"2018-11-07T00:00:00.000Z", :wma=>207.11866666666668}, + {:date_time=>"2018-11-06T00:00:00.000Z", :wma=>207.39666666666665}, + {:date_time=>"2018-11-05T00:00:00.000Z", :wma=>210.37}, + {:date_time=>"2018-11-02T00:00:00.000Z", :wma=>214.78}, + {:date_time=>"2018-11-01T00:00:00.000Z", :wma=>217.81466666666665}, + {:date_time=>"2018-10-31T00:00:00.000Z", :wma=>215.7746666666667}, + {:date_time=>"2018-10-30T00:00:00.000Z", :wma=>214.60333333333332}, + {:date_time=>"2018-10-29T00:00:00.000Z", :wma=>215.91400000000002}, + {:date_time=>"2018-10-26T00:00:00.000Z", :wma=>218.13866666666667}, + {:date_time=>"2018-10-25T00:00:00.000Z", :wma=>219.21066666666667}, + {:date_time=>"2018-10-24T00:00:00.000Z", :wma=>218.86400000000003}, + {:date_time=>"2018-10-23T00:00:00.000Z", :wma=>220.49400000000003}, + {:date_time=>"2018-10-22T00:00:00.000Z", :wma=>219.53866666666664}, + {:date_time=>"2018-10-19T00:00:00.000Z", :wma=>219.05733333333333}, + {:date_time=>"2018-10-18T00:00:00.000Z", :wma=>219.20933333333335}, + {:date_time=>"2018-10-17T00:00:00.000Z", :wma=>220.35333333333335}, + {:date_time=>"2018-10-16T00:00:00.000Z", :wma=>219.452}, + {:date_time=>"2018-10-15T00:00:00.000Z", :wma=>218.54533333333333} + ] + + 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 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('wma') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Weighted Moving Average') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period price_key date_time_key)) + end + + it 'Validates options' do + valid_options = { period: 22, price_key: :close, date_time_key: :timestep } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(**options)).to eq(4) + end + end + end +end diff --git a/spec/technical_analysis/indicators/wr_spec.rb b/spec/technical_analysis/indicators/wr_spec.rb new file mode 100644 index 0000000..b2317d3 --- /dev/null +++ b/spec/technical_analysis/indicators/wr_spec.rb @@ -0,0 +1,106 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "WR" do + input_data = SpecHelper.get_test_data(:high, :low, :close) + indicator = TechnicalAnalysis::Wr + + describe 'Williams %R' do + it 'Calculates Williams %R' do + output = indicator.calculate(input_data, period: 14) + normalized_output = output.map(&:to_hash) + + expected_output = [ + {:date_time=>"2019-01-09T00:00:00.000Z", :wr=>-55.55992141453828}, + {:date_time=>"2019-01-08T00:00:00.000Z", :wr=>-65.72659616137877}, + {:date_time=>"2019-01-07T00:00:00.000Z", :wr=>-77.4952561669829}, + {:date_time=>"2019-01-04T00:00:00.000Z", :wr=>-76.88330871491881}, + {:date_time=>"2019-01-03T00:00:00.000Z", :wr=>-99.37847562970234}, + {:date_time=>"2019-01-02T00:00:00.000Z", :wr=>-56.38953040800621}, + {:date_time=>"2018-12-31T00:00:00.000Z", :wr=>-57.082371054657386}, + {:date_time=>"2018-12-28T00:00:00.000Z", :wr=>-62.8945342571209}, + {:date_time=>"2018-12-27T00:00:00.000Z", :wr=>-65.7347670250896}, + {:date_time=>"2018-12-26T00:00:00.000Z", :wr=>-62.468960624334926}, + {:date_time=>"2018-12-24T00:00:00.000Z", :wr=>-99.32960706594149}, + {:date_time=>"2018-12-21T00:00:00.000Z", :wr=>-96.88473520249222}, + {:date_time=>"2018-12-20T00:00:00.000Z", :wr=>-94.83805668016194}, + {:date_time=>"2018-12-19T00:00:00.000Z", :wr=>-93.03675048355906}, + {:date_time=>"2018-12-18T00:00:00.000Z", :wr=>-84.961728950923}, + {:date_time=>"2018-12-17T00:00:00.000Z", :wr=>-94.55200360198106}, + {:date_time=>"2018-12-14T00:00:00.000Z", :wr=>-90.0509023600186}, + {:date_time=>"2018-12-13T00:00:00.000Z", :wr=>-64.73854696899592}, + {:date_time=>"2018-12-12T00:00:00.000Z", :wr=>-73.29939842665439}, + {:date_time=>"2018-12-11T00:00:00.000Z", :wr=>-75.47431744562708}, + {:date_time=>"2018-12-10T00:00:00.000Z", :wr=>-77.09170624771653}, + {:date_time=>"2018-12-07T00:00:00.000Z", :wr=>-99.28757569508241}, + {:date_time=>"2018-12-06T00:00:00.000Z", :wr=>-81.95026204496244}, + {:date_time=>"2018-12-04T00:00:00.000Z", :wr=>-73.97761994374633}, + {:date_time=>"2018-12-03T00:00:00.000Z", :wr=>-45.9138187221397}, + {:date_time=>"2018-11-30T00:00:00.000Z", :wr=>-71.8823927002365}, + {:date_time=>"2018-11-29T00:00:00.000Z", :wr=>-74.01398601398596}, + {:date_time=>"2018-11-28T00:00:00.000Z", :wr=>-73.20622177621675}, + {:date_time=>"2018-11-27T00:00:00.000Z", :wr=>-90.01505268439534}, + {:date_time=>"2018-11-26T00:00:00.000Z", :wr=>-89.06171600602104}, + {:date_time=>"2018-11-23T00:00:00.000Z", :wr=>-99.50026301946345}, + {:date_time=>"2018-11-21T00:00:00.000Z", :wr=>-96.67016255899316}, + {:date_time=>"2018-11-20T00:00:00.000Z", :wr=>-96.8623265741729}, + {:date_time=>"2018-11-19T00:00:00.000Z", :wr=>-97.67192935509766}, + {:date_time=>"2018-11-16T00:00:00.000Z", :wr=>-79.13807301674446}, + {:date_time=>"2018-11-15T00:00:00.000Z", :wr=>-84.95745264891575}, + {:date_time=>"2018-11-14T00:00:00.000Z", :wr=>-97.61185835849572}, + {:date_time=>"2018-11-13T00:00:00.000Z", :wr=>-97.47686016454276}, + {:date_time=>"2018-11-12T00:00:00.000Z", :wr=>-98.75164257555849}, + {:date_time=>"2018-11-09T00:00:00.000Z", :wr=>-75.82501918649267}, + {:date_time=>"2018-11-08T00:00:00.000Z", :wr=>-60.39907904834988}, + {:date_time=>"2018-11-07T00:00:00.000Z", :wr=>-54.79662317728319}, + {:date_time=>"2018-11-06T00:00:00.000Z", :wr=>-78.51112816577121}, + {:date_time=>"2018-11-05T00:00:00.000Z", :wr=>-86.87643898695312}, + {:date_time=>"2018-11-02T00:00:00.000Z", :wr=>-89.09574468085114}, + {:date_time=>"2018-11-01T00:00:00.000Z", :wr=>-11.080485115766221}, + {:date_time=>"2018-10-31T00:00:00.000Z", :wr=>-29.60308710033065}, + {:date_time=>"2018-10-30T00:00:00.000Z", :wr=>-60.253583241455274}, + {:date_time=>"2018-10-29T00:00:00.000Z", :wr=>-69.64461994076994}, + {:date_time=>"2018-10-26T00:00:00.000Z", :wr=>-73.3779264214046} + ] + + 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)}.to raise_exception(TechnicalAnalysis::Validation::ValidationError) + end + + it 'Returns the symbol' do + indicator_symbol = indicator.indicator_symbol + expect(indicator_symbol).to eq('wr') + end + + it 'Returns the name' do + indicator_name = indicator.indicator_name + expect(indicator_name).to eq('Williams %R') + end + + it 'Returns the valid options' do + valid_options = indicator.valid_options + expect(valid_options).to eq(%i(period)) + end + + it 'Validates options' do + valid_options = { period: 22 } + options_validated = indicator.validate_options(valid_options) + expect(options_validated).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 'Calculates minimum data size' do + options = { period: 4 } + expect(indicator.min_data_size(options)).to eq(4) + end + end + end +end diff --git a/technical-analysis.gemspec b/technical-analysis.gemspec index 5d246e2..099220f 100644 --- a/technical-analysis.gemspec +++ b/technical-analysis.gemspec @@ -1,17 +1,17 @@ Gem::Specification.new do |spec| spec.name = "technical-analysis" - spec.version = "1.0.0" + spec.version = "0.2.3" spec.authors = ["Intrinio"] spec.email = ["admin@intrinio.com"] - spec.description = %q{Intrinio Technical Analysis} - spec.summary = %q{Intrinio Technical Analysis} + spec.description = %q{A Ruby library for performing technical analysis on stock prices and other data sets.} + spec.summary = %q{A Ruby library for performing technical analysis on stock prices and other data sets.} spec.homepage = "https://github.com/intrinio/technical-analysis" spec.files = Dir["{spec,lib}/**/*.*"] spec.require_path = "lib" - spec.metadata['allowed_push_host'] = 'https://intrinio.com' + spec.metadata['allowed_push_host'] = 'https://rubygems.org' spec.add_development_dependency "bundler", "~> 1.16" - spec.add_development_dependency "rake", "~> 10.0" + spec.add_development_dependency "rake", "~> 12.3" spec.add_development_dependency "rspec", "~> 3.0" - spec.add_development_dependency "yard", "~> 0.9.6" -end \ No newline at end of file + spec.add_development_dependency "yard", "~> 0.9.20" +end