Skip to content
Closed
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
3 changes: 2 additions & 1 deletion lib/technical_analysis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
require 'technical_analysis/helpers/validation'

# Indicators
require 'technical_analysis/indicators/sma'
require 'technical_analysis/indicators/sma'
require 'technical_analysis/indicators/adi'
8 changes: 8 additions & 0 deletions lib/technical_analysis/helpers/array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 5 additions & 3 deletions lib/technical_analysis/helpers/validation.rb
Original file line number Diff line number Diff line change
@@ -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

Expand Down
33 changes: 33 additions & 0 deletions lib/technical_analysis/indicators/adi.rb
Original file line number Diff line number Diff line change
@@ -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
21 changes: 11 additions & 10 deletions lib/technical_analysis/indicators/sma.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -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


64 changes: 64 additions & 0 deletions spec/ta_test_data.csv
Original file line number Diff line number Diff line change
@@ -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
83 changes: 83 additions & 0 deletions spec/technical_analysis/indicators/adi_spec.rb
Original file line number Diff line number Diff line change
@@ -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
98 changes: 65 additions & 33 deletions spec/technical_analysis/indicators/sma_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down