Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/technical_analysis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@
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'
8 changes: 8 additions & 0 deletions lib/technical_analysis/helpers/stock_calculation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,13 @@ def self.ema(current_value, data, period, 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
intermediate_values.sum
end

end
end
100 changes: 100 additions & 0 deletions lib/technical_analysis/indicators/wma.rb
Original file line number Diff line number Diff line change
@@ -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<WmaValue>] 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
115 changes: 115 additions & 0 deletions spec/technical_analysis/indicators/wma_spec.rb
Original file line number Diff line number Diff line change
@@ -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.88066666666666},
{:date_time=>"2018-12-26T00:00:00.000Z", :wma=>153.32733333333334},
{:date_time=>"2018-12-24T00:00:00.000Z", :wma=>153.02733333333333},
{:date_time=>"2018-12-21T00:00:00.000Z", :wma=>157.31466666666665},
{: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.35733333333332},
{: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.28799999999998},
{: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.90066666666667},
{:date_time=>"2018-12-03T00:00:00.000Z", :wma=>180.87933333333334},
{:date_time=>"2018-11-30T00:00:00.000Z", :wma=>178.46800000000002},
{:date_time=>"2018-11-29T00:00:00.000Z", :wma=>177.71733333333333},
{:date_time=>"2018-11-28T00:00:00.000Z", :wma=>176.4586666666667},
{: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.58333333333334},
{: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.11866666666666},
{: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.60333333333335},
{: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.864},
{:date_time=>"2018-10-23T00:00:00.000Z", :wma=>220.494},
{:date_time=>"2018-10-22T00:00:00.000Z", :wma=>219.53866666666667},
{: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.54533333333336}
]

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