diff --git a/lib/technical_analysis.rb b/lib/technical_analysis.rb index fed0241..9bb42da 100644 --- a/lib/technical_analysis.rb +++ b/lib/technical_analysis.rb @@ -5,4 +5,5 @@ require 'technical_analysis/helpers/validation' # Indicators -require 'technical_analysis/indicators/sma' \ No newline at end of file +require 'technical_analysis/indicators/sma' +require 'technical_analysis/indicators/adi' \ No newline at end of file diff --git a/lib/technical_analysis/helpers/array.rb b/lib/technical_analysis/helpers/array.rb index 8d60578..bc6e04c 100644 --- a/lib/technical_analysis/helpers/array.rb +++ b/lib/technical_analysis/helpers/array.rb @@ -2,4 +2,12 @@ class Array def sum self.inject(0, :+) end + + def sort_by_hash_date_asc + self.sort_by {|row| row[:date]} + end + + def sort_by_hash_date_desc + sort_by_hash_date_asc.reverse + end end \ No newline at end of file diff --git a/lib/technical_analysis/helpers/validation.rb b/lib/technical_analysis/helpers/validation.rb index 339fe2b..d122f5c 100644 --- a/lib/technical_analysis/helpers/validation.rb +++ b/lib/technical_analysis/helpers/validation.rb @@ -1,8 +1,10 @@ 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 diff --git a/lib/technical_analysis/indicators/adi.rb b/lib/technical_analysis/indicators/adi.rb new file mode 100644 index 0000000..310e14a --- /dev/null +++ b/lib/technical_analysis/indicators/adi.rb @@ -0,0 +1,33 @@ +module TechnicalAnalysis + class Adi + # Calculates the Accumulation/Distribution Index for the given data + # https://en.wikipedia.org/wiki/Accumulation/distribution_index + # + # @param data [Array] Array of hashes with keys (:date, :high, :low, :close, :volume) + # @return [Hash] A hash of the results with keys (:date, :value) + def self.calculate(data) + Validation.validate_numeric_data(data, :high, :low, :close, :volume) + + data = data.sort_by_hash_date_asc + + ads = [] + + clv = 0 + ad = 0 + 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 + + ads << {date: values[:date], value: ad} + end + ads + end + end +end diff --git a/lib/technical_analysis/indicators/sma.rb b/lib/technical_analysis/indicators/sma.rb index 813ee96..ec6c2f5 100644 --- a/lib/technical_analysis/indicators/sma.rb +++ b/lib/technical_analysis/indicators/sma.rb @@ -3,21 +3,22 @@ class Sma # Calculates the simple moving average 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, :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) + # @param price_key [Symbol] The hash key for the price data. Default :value + # @return [Hash] A hash of the results with keys (:date, :value) + def self.calculate(data, period: 30, price_key: :value) + Validation.validate_numeric_data(data, price_key) Validation.validate_length(data, period) - output = {} - period_values = [] - data = data.sort.to_h # Sort data by descending dates + data = data.sort_by_hash_date_asc # Sort data by descending dates - data.each do |date, price| - period_values << price + output = [] + period_values = [] + data.each do |v| + period_values << v[price_key] if period_values.size == period - output[date] = period_values.sum / period.to_f + output << {date: v[:date], value: period_values.sum / period.to_f} period_values.shift end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..34f4bb5 --- /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] + INTEGER_KEYS = [:volume] + + def self.get_test_data(*columns) + @data = CSV.read(TEST_DATA_PATH, headers: true) + columns = columns.map(&:to_sym) + output = [] + @data.each do |v| + col_hash = {date: v["date"]} + 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..60f8763 --- /dev/null +++ b/spec/ta_test_data.csv @@ -0,0 +1,64 @@ +date,close,volume,open,high,low +"2019/01/09",153.3100,45034370.0000,151.2900,154.5300,149.6300 +"2019/01/08",150.7500,40622910.0000,149.5600,151.8200,148.5200 +"2019/01/07",147.9300,54571440.0000,148.7000,148.8300,145.9000 +"2019/01/04",148.2600,57423650.0000,144.5300,148.5499,143.8000 +"2019/01/03",142.1900,91106840.0000,143.9800,145.7200,142.0000 +"2019/01/02",157.9200,35637070.0000,154.8900,158.8500,154.2300 +"2018/12/31",157.7400,34499390.0000,158.5300,159.3600,156.4800 +"2018/12/28",156.2300,41740600.0000,157.5000,158.5200,154.5500 +"2018/12/27",156.1500,51608850.0000,155.8400,156.7700,150.0700 +"2018/12/26",157.1700,58133850.0000,148.3000,157.2300,146.7200 +"2018/12/24",146.8300,37169230.0000,148.1500,151.5500,146.5900 +"2018/12/21",150.7300,95497900.0000,156.8600,158.1600,149.6300 +"2018/12/20",156.8300,64398230.0000,160.4000,162.1100,155.3000 +"2018/12/19",160.8900,47597670.0000,166.0000,167.4500,159.0900 +"2018/12/18",166.0700,33753490.0000,165.3800,167.5300,164.3900 +"2018/12/17",163.9400,43250420.0000,165.4500,168.3500,162.7300 +"2018/12/14",165.4800,40620360.0000,169.0000,169.0800,165.2800 +"2018/12/13",170.9500,31754210.0000,170.4900,172.5700,169.5500 +"2018/12/12",169.1000,35474680.0000,170.4000,171.9200,169.0200 +"2018/12/11",168.6300,45968040.0000,171.6600,171.7900,167.0000 +"2018/12/10",169.6000,61759000.0000,165.0000,170.0900,163.3300 +"2018/12/07",168.4900,41678680.0000,173.4900,174.4900,168.3000 +"2018/12/06",174.7200,42704910.0000,171.7600,174.7800,170.4200 +"2018/12/04",176.6900,41141250.0000,180.9500,182.3899,176.2700 +"2018/12/03",184.8200,40537700.0000,184.4600,184.9400,181.2100 +"2018/11/30",178.5800,39424260.0000,180.2900,180.3300,177.0300 +"2018/11/29",179.5500,41523580.0000,182.6600,182.8000,177.7000 +"2018/11/28",180.9400,45941750.0000,176.7300,181.2900,174.9300 +"2018/11/27",174.2400,41156140.0000,171.5100,174.7700,170.8800 +"2018/11/26",174.6200,44662320.0000,174.2400,174.9500,170.2600 +"2018/11/23",172.2900,23623970.0000,174.9400,176.5950,172.1000 +"2018/11/21",176.7800,31096240.0000,179.7300,180.2700,176.5500 +"2018/11/20",176.9800,67678680.0000,178.3700,181.4700,175.5100 +"2018/11/19",185.8600,41626820.0000,190.0000,190.7000,184.9900 +"2018/11/16",193.5300,36191330.0000,190.5000,194.9695,189.4600 +"2018/11/15",191.4100,46271660.0000,188.3900,191.9700,186.9000 +"2018/11/14",186.8000,60547340.0000,193.9000,194.4800,185.9300 +"2018/11/13",192.2300,46725710.0000,191.6300,197.1800,191.4501 +"2018/11/12",194.1700,50991030.0000,199.0000,199.8500,193.7900 +"2018/11/09",204.4700,34317760.0000,205.5500,206.0100,202.2500 +"2018/11/08",208.4900,25289270.0000,209.9800,210.1200,206.7500 +"2018/11/07",209.9500,33291640.0000,205.9700,210.0600,204.1300 +"2018/11/06",203.7700,31774720.0000,201.9200,204.7200,201.6900 +"2018/11/05",201.5900,66072170.0000,204.3000,204.3900,198.1700 +"2018/11/02",207.4800,91046560.0000,209.5500,213.6500,205.4300 +"2018/11/01",222.2200,52954070.0000,219.0500,222.3600,216.8100 +"2018/10/31",218.8600,38016810.0000,216.8800,220.4500,216.6200 +"2018/10/30",213.3000,36487930.0000,211.1500,215.1800,209.2700 +"2018/10/29",212.2400,45713690.0000,219.1900,219.6900,206.0900 +"2018/10/26",216.3000,47191700.0000,215.9000,220.1900,212.6700 +"2018/10/25",219.8000,29027340.0000,217.7100,221.3800,216.7500 +"2018/10/24",215.0900,39992120.0000,222.6000,224.2300,214.5400 +"2018/10/23",222.7300,38681170.0000,215.8300,223.2500,214.7000 +"2018/10/22",220.6500,28751540.0000,219.7900,223.3600,218.9400 +"2018/10/19",219.3100,32874330.0000,218.0600,221.2600,217.4300 +"2018/10/18",216.0200,32389280.0000,217.8600,219.7400,213.0000 +"2018/10/17",221.1900,22692880.0000,222.3000,222.6400,219.3400 +"2018/10/16",222.1500,28802550.0000,218.9300,222.9900,216.7627 +"2018/10/15",217.3600,30280450.0000,221.1600,221.8300,217.2700 +"2018/10/12",222.1100,39494770.0000,220.4200,222.8800,216.8400 +"2018/10/11",214.4500,52902320.0000,214.5200,219.5000,212.3200 +"2018/10/10",216.3600,41084070.0000,225.4600,226.3500,216.0500 +"2018/10/09",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..f6c0b9d --- /dev/null +++ b/spec/technical_analysis/indicators/adi_spec.rb @@ -0,0 +1,83 @@ +require 'technical-analysis' +require 'spec_helper' + +describe 'Indicators' do + describe "ADI" do + + describe 'Accumulation/Distribution Index' do + it 'Calculates ADI' do + + input_data = SpecHelper.get_test_data(:volume, :high, :low, :close) + output = TechnicalAnalysis::Adi.calculate(input_data) + + expected_output = [ + {:date=>"2018/10/09", :value=>22411774.711174767}, + {:date=>"2018/10/10", :value=>-16199273.599504825}, + {:date=>"2018/10/11", :value=>-37713866.134323865}, + {:date=>"2018/10/12", :value=>-8288954.710482582}, + {:date=>"2018/10/15", :value=>-37374123.7894299}, + {:date=>"2018/10/16", :value=>-16341921.130974408}, + {:date=>"2018/10/17", :value=>-13591269.009762233}, + {:date=>"2018/10/18", :value=>-16955140.819851194}, + {:date=>"2018/10/19", :value=>-17555977.138388995}, + {:date=>"2018/10/22", :value=>-24060850.441556394}, + {:date=>"2018/10/23", :value=>9915241.57013943}, + {:date=>"2018/10/24", :value=>-25537009.286413625}, + {:date=>"2018/10/25", :value=>-16320985.571510637}, + {:date=>"2018/10/26", :value=>-17952613.497042313}, + {:date=>"2018/10/29", :value=>-22322304.45292461}, + {:date=>"2018/10/30", :value=>-9048353.606900878}, + {:date=>"2018/10/31", :value=>-2596414.5729579534}, + {:date=>"2018/11/01", :value=>47686098.74235708}, + {:date=>"2018/11/02", :value=>2052056.5039138943}, + {:date=>"2018/11/05", :value=>8638028.433174696}, + {:date=>"2018/11/06", :value=>20488006.51898352}, + {:date=>"2018/11/07", :value=>52544543.51729703}, + {:date=>"2018/11/08", :value=>53370009.30364728}, + {:date=>"2018/11/09", :value=>59576412.70790268}, + {:date=>"2018/11/12", :value=>14980297.36136794}, + {:date=>"2018/11/13", :value=>-19025685.861899547}, + {:date=>"2018/11/14", :value=>-67251111.0548819}, + {:date=>"2018/11/15", :value=>-31201198.431607798}, + {:date=>"2018/11/16", :value=>-13921718.702957403}, + {:date=>"2018/11/19", :value=>-42863658.35269459}, + {:date=>"2018/11/20", :value=>-77157217.68155372}, + {:date=>"2018/11/21", :value=>-104408223.70305927}, + {:date=>"2018/11/23", :value=>-126035061.64521725}, + {:date=>"2018/11/26", :value=>-87657844.24649628}, + {:date=>"2018/11/27", :value=>-57716487.89688186}, + {:date=>"2018/11/28", :value=>-16831219.815120786}, + {:date=>"2018/11/29", :value=>-28229849.61904212}, + {:date=>"2018/11/30", :value=>-30619198.70995107}, + {:date=>"2018/12/03", :value=>7310177.429459017}, + {:date=>"2018/12/04", :value=>-28184142.065140743}, + {:date=>"2018/12/06", :value=>13345403.439446371}, + {:date=>"2018/12/07", :value=>-25774650.00158758}, + {:date=>"2018/12/10", :value=>27031122.187761355}, + {:date=>"2018/12/11", :value=>12348220.058324995}, + {:date=>"2018/12/12", :value=>-21169236.217537448}, + {:date=>"2018/12/13", :value=>-23482456.813564237}, + {:date=>"2018/12/14", :value=>-59826989.44514345}, + {:date=>"2018/12/17", :value=>-84453563.11062376}, + {:date=>"2018/12/18", :value=>-82088668.90680213}, + {:date=>"2018/12/19", :value=>-109189734.6005822}, + {:date=>"2018/12/20", :value=>-144651314.99705797}, + {:date=>"2018/12/21", :value=>-215519041.4917826}, + {:date=>"2018/12/24", :value=>-249091249.23371798}, + {:date=>"2018/12/26", :value=>-191621153.9435182}, + {:date=>"2018/12/27", :value=>-149563792.60023466}, + {:date=>"2018/12/28", :value=>-155977335.67328295}, + {:date=>"2018/12/31", :value=>-160289759.42328274}, + {:date=>"2019/01/02", :value=>-139000081.24146464}, + {:date=>"2019/01/03", :value=>-220800308.5532927}, + {:date=>"2019/01/04", :value=>-170386118.17770624}, + {:date=>"2019/01/07", :value=>-149339794.90125585}, + {:date=>"2019/01/08", :value=>-135060226.53761944}, + {:date=>"2019/01/09", :value=>-112451134.66006838} + ] + + expect(output).to eq(expected_output) + end + end + end +end diff --git a/spec/technical_analysis/indicators/sma_spec.rb b/spec/technical_analysis/indicators/sma_spec.rb index 5c8dd29..2b24399 100644 --- a/spec/technical_analysis/indicators/sma_spec.rb +++ b/spec/technical_analysis/indicators/sma_spec.rb @@ -1,49 +1,81 @@ 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) describe 'Simple Moving Average' do it 'Calculates SMA (5 day)' do - output = TechnicalAnalysis::Sma.calculate(input_data, period: 5) + output = TechnicalAnalysis::Sma.calculate(input_data, period: 5, price_key: :close) - 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=>"2018/10/15", :value=>219.43}, + {:date=>"2018/10/16", :value=>218.48600000000002}, + {:date=>"2018/10/17", :value=>219.452}, + {:date=>"2018/10/18", :value=>219.766}, + {:date=>"2018/10/19", :value=>219.206}, + {:date=>"2018/10/22", :value=>219.86400000000003}, + {:date=>"2018/10/23", :value=>219.97999999999996}, + {:date=>"2018/10/24", :value=>218.76}, + {:date=>"2018/10/25", :value=>219.51600000000002}, + {:date=>"2018/10/26", :value=>218.914}, + {:date=>"2018/10/29", :value=>217.23200000000003}, + {:date=>"2018/10/30", :value=>215.346}, + {:date=>"2018/10/31", :value=>216.1}, + {:date=>"2018/11/01", :value=>216.584}, + {:date=>"2018/11/02", :value=>214.82000000000002}, + {:date=>"2018/11/05", :value=>212.69}, + {:date=>"2018/11/06", :value=>210.78400000000002}, + {:date=>"2018/11/07", :value=>209.002}, + {:date=>"2018/11/08", :value=>206.256}, + {:date=>"2018/11/09", :value=>205.654}, + {:date=>"2018/11/12", :value=>204.17000000000002}, + {:date=>"2018/11/13", :value=>201.862}, + {:date=>"2018/11/14", :value=>197.23200000000003}, + {:date=>"2018/11/15", :value=>193.816}, + {:date=>"2018/11/16", :value=>191.628}, + {:date=>"2018/11/19", :value=>189.96599999999998}, + {:date=>"2018/11/20", :value=>186.916}, + {:date=>"2018/11/21", :value=>184.91199999999998}, + {:date=>"2018/11/23", :value=>181.088}, + {:date=>"2018/11/26", :value=>177.30599999999998}, + {:date=>"2018/11/27", :value=>174.982}, + {:date=>"2018/11/28", :value=>175.77400000000003}, + {:date=>"2018/11/29", :value=>176.32799999999997}, + {:date=>"2018/11/30", :value=>177.58599999999998}, + {:date=>"2018/12/03", :value=>179.62600000000003}, + {:date=>"2018/12/04", :value=>180.11600000000004}, + {:date=>"2018/12/06", :value=>178.872}, + {:date=>"2018/12/07", :value=>176.66}, + {:date=>"2018/12/10", :value=>174.864}, + {:date=>"2018/12/11", :value=>171.626}, + {:date=>"2018/12/12", :value=>170.108}, + {:date=>"2018/12/13", :value=>169.35399999999998}, + {:date=>"2018/12/14", :value=>168.752}, + {:date=>"2018/12/17", :value=>167.61999999999998}, + {:date=>"2018/12/18", :value=>167.108}, + {:date=>"2018/12/19", :value=>165.46599999999998}, + {:date=>"2018/12/20", :value=>162.642}, + {:date=>"2018/12/21", :value=>159.692}, + {:date=>"2018/12/24", :value=>156.27}, + {:date=>"2018/12/26", :value=>154.49}, + {:date=>"2018/12/27", :value=>153.54199999999997}, + {:date=>"2018/12/28", :value=>153.422}, + {:date=>"2018/12/31", :value=>154.824}, + {:date=>"2019/01/02", :value=>157.04199999999997}, + {:date=>"2019/01/03", :value=>154.046}, + {:date=>"2019/01/04", :value=>152.468}, + {:date=>"2019/01/07", :value=>150.808}, + {:date=>"2019/01/08", :value=>149.41}, + {:date=>"2019/01/09", :value=>148.488} + ] expect(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 {TechnicalAnalysis::Sma.calculate(input_data, period: input_data.size+1, price_key: :close)}.to raise_exception(Validation::ValidationError) end end end