├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── indicators.gemspec ├── lib ├── indicators.rb └── indicators │ ├── calculations │ ├── bb.rb │ ├── ema.rb │ ├── helper.rb │ ├── macd.rb │ ├── rsi.rb │ ├── sma.rb │ └── sto.rb │ ├── data.rb │ ├── main.rb │ ├── parser.rb │ └── version.rb └── spec ├── data_spec.rb ├── helper_spec.rb ├── main_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 1.9.2 4 | script: "bundle exec rake" -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in indicators.gemspec 4 | gemspec 5 | gem 'guard' 6 | gem 'guard-rspec' 7 | gem 'guard-bundler' 8 | gem 'growl' 9 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'bundler' do 5 | watch('Gemfile') 6 | # Uncomment next line if Gemfile contain `gemspec' command 7 | # watch(/^.+\.gemspec/) 8 | end 9 | 10 | guard 'rspec', :version => 2 do 11 | watch(%r{^spec/.+_spec\.rb$}) 12 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 13 | watch('spec/spec_helper.rb') { "spec" } 14 | 15 | # Rails example 16 | watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 17 | watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } 18 | watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } 19 | watch(%r{^spec/support/(.+)\.rb$}) { "spec" } 20 | watch('config/routes.rb') { "spec/routing" } 21 | watch('app/controllers/application_controller.rb') { "spec/controllers" } 22 | 23 | # Capybara request specs 24 | watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } 25 | 26 | # Turnip features and steps 27 | watch(%r{^spec/acceptance/(.+)\.feature$}) 28 | watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' } 29 | end 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Nedomas 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Indicators 2 | 3 | A gem for calculating technical analysis indicators. 4 | 5 | Current functionality demo of Indicators and Securities gems synergy can be tested at http://strangemuseum.heroku.com. 6 | Securities gem: https://rubygems.org/gems/securities 7 | 8 | [![Build Status](https://secure.travis-ci.org/Nedomas/indicators.png)](http://travis-ci.org/Nedomas/indicators)[![Dependency Status](https://gemnasium.com/Nedomas/indicators.png)](https://gemnasium.com/Nedomas/indicators) 9 | 10 | ## Installation 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | gem 'indicators' 15 | 16 | And then execute: 17 | 18 | $ bundle 19 | 20 | Or install it yourself as: 21 | 22 | $ gem install indicators 23 | 24 | ## Accepted Input 25 | 26 | # Array 27 | 28 | my_data = Indicators::Data.new([1, 2, 3, 4, 5]) 29 | 30 | Then it returns data as an array with indicator values in index places: 31 | 32 | i.e. my_data.calc(:type => :sma, :params => 2).output => [nil, 1.5, 2.5, 3.5, 4.5] 33 | 34 | # Securities gem hash 35 | 36 | my_data = Indicators::Data.new(Securities::Stock.new(:symbol => 'AAPL', :start_date => '2012-08-25', :end_date => '2012-08-30').output) 37 | 38 | my_data.calc(:type => :sma, :params => 3) 39 | 40 | ## Output 41 | 42 | .calc returns an object with such accessor methods 43 | 44 | @abbr - indicator abbreviation (usually used when displaying information). 45 | @params - given or defaulted parameters. 46 | @output - indicator calculation result. 47 | 48 | i.e. @abbr="SMA", @params=2, @output=[nil, 1.5, 2.5, 3.5, 4.5] 49 | 50 | ## Supported Indicators 51 | 52 | # Simple Moving Average => :sma 53 | 54 | my_data.calc(:type => :sma, :params => 5) 55 | 56 | # Exponental Moving Average => :ema 57 | 58 | my_data.calc(:type => :ema, :params => 5) 59 | 60 | # Bollinger Bands => :bb 61 | 62 | my_data.calc(:type => :bb, :params => [15, 3]) or my_data.calc(:type => :bb, :params => [15, 2.5]) 63 | 64 | Variables have to be specified as an array [periods, multiplier]. If multiplier isn't specified, it defaults to 2. 65 | 66 | It returns output as an array for each data point [middle band, upper band, lower band]. 67 | i.e. my_data.calc(:type => :bb, :params => 3) => {"aapl"=>[nil, nil, [674.65, 676.8752190903368, 672.4247809096631]]} 68 | 69 | # Moving Average Convergence Divergence => :macd 70 | 71 | my_data.calc(:type => :macd, :params => [12, 26, 9]) 72 | 73 | Variables have to be specified as an array [faster periods, slower periods, signal line]. If slower periods isn't specified, it defaults to 26 and signal line to 9. 74 | 75 | MACD output is [MACD line, signal line, MACD histogram]. 76 | 77 | # Relative Strength Index => :rsi 78 | 79 | my_data.calc(:type => :rsi, :params => 14) 80 | 81 | The more data it has, the more accurate RSI is. 82 | 83 | # Full Stochastic Oscillator => :sto 84 | 85 | my_data.calc(:type => :sto, :params => [14, 3, 5]) 86 | 87 | Variables have to be specified as an array [lookback period, the number of periods to slow %K, the number of periods for the %D moving average] => [%K1, %K2, %D]. 88 | 89 | Stochastic output is [fast %K, slow %K, full %D]. 90 | 91 | ## To Do 92 | 93 | * A strategy backtesting tool. 94 | 95 | # Indicators: 96 | * More moving averages (CMA, WMA, MMA). 97 | * ROC. 98 | * CCI. 99 | * Williams %R. 100 | * ADX. 101 | * Parabolic SAR. 102 | * StochRSI. 103 | 104 | ## Contributing 105 | 106 | 1. Fork it 107 | 2. Create your feature branch (`git checkout -b my-new-feature`) 108 | 3. Commit your changes (`git commit -am 'Added some feature'`) 109 | 4. Push to the branch (`git push origin my-new-feature`) 110 | 5. Create new Pull Request 111 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | 4 | require 'rspec/core/rake_task' 5 | RSpec::Core::RakeTask.new 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /indicators.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/indicators/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Nedomas"] 6 | gem.email = ["domas.bitvinskas@me.com"] 7 | gem.description = %q{A gem for calculating technical analysis indicators.} 8 | gem.summary = %q{A gem for calculating technical analysis indicators.} 9 | gem.homepage = "http://github.com/Nedomas/indicators" 10 | 11 | gem.files = `git ls-files`.split($\) 12 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 13 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 14 | gem.name = "indicators" 15 | gem.require_paths = ["lib"] 16 | gem.version = Indicators::VERSION 17 | 18 | gem.add_dependency 'rails' 19 | gem.add_development_dependency 'securities', ">= 2.0.0", "< 3.0.0" 20 | gem.add_development_dependency 'rspec' 21 | end 22 | -------------------------------------------------------------------------------- /lib/indicators.rb: -------------------------------------------------------------------------------- 1 | require "indicators/version" 2 | require "securities" 3 | require "indicators/data.rb" 4 | require "indicators/parser.rb" 5 | require "indicators/main.rb" 6 | 7 | require "indicators/calculations/helper.rb" 8 | 9 | # Lagging Indicators 10 | require "indicators/calculations/sma.rb" 11 | require "indicators/calculations/ema.rb" 12 | require "indicators/calculations/bb.rb" 13 | require "indicators/calculations/macd.rb" 14 | 15 | # Leading Indicators 16 | require "indicators/calculations/rsi.rb" 17 | require "indicators/calculations/sto.rb" 18 | 19 | module Indicators 20 | end 21 | -------------------------------------------------------------------------------- /lib/indicators/calculations/bb.rb: -------------------------------------------------------------------------------- 1 | module Indicators 2 | # 3 | # Bollinger Bands 4 | class Bb 5 | 6 | # Middle Band = 20-day simple moving average (SMA) 7 | # Upper Band = 20-day SMA + (20-day standard deviation of price x 2) 8 | # Lower Band = 20-day SMA - (20-day standard deviation of price x 2) 9 | def self.calculate data, parameters 10 | periods = parameters[0] 11 | multiplier = parameters[1] 12 | output = Array.new 13 | adj_closes = Indicators::Helper.validate_data(data, :adj_close, periods) 14 | 15 | adj_closes.each_with_index do |adj_close, index| 16 | start = index+1-periods 17 | if index+1 >= periods 18 | middle_band = Indicators::Sma.calculate(adj_closes[start..index], periods).last 19 | upper_band = middle_band + (adj_closes[start..index].standard_deviation * multiplier) 20 | lower_band = middle_band - (adj_closes[start..index].standard_deviation * multiplier) 21 | # Output for each point is [middle, upper, lower]. 22 | output[index] = [middle_band, upper_band, lower_band] 23 | else 24 | output[index] = nil 25 | end 26 | end 27 | 28 | return output 29 | 30 | end 31 | 32 | end 33 | end -------------------------------------------------------------------------------- /lib/indicators/calculations/ema.rb: -------------------------------------------------------------------------------- 1 | module Indicators 2 | # 3 | # Exponential Moving Average 4 | class Ema 5 | 6 | # Multiplier: (2 / (Time periods + 1) ) = (2 / (10 + 1) ) = 0.1818 (18.18%) 7 | # EMA: {Close - EMA(previous day)} x multiplier + EMA(previous day). 8 | def self.calculate data, parameters 9 | periods = parameters 10 | output = Array.new 11 | adj_closes = Indicators::Helper.validate_data(data, :adj_close, periods) 12 | 13 | k = 2.0/(periods+1) 14 | adj_closes.each_with_index do |adj_close, index| 15 | start = index+1-periods 16 | if start == 0 17 | output[index] = Indicators::Sma.calculate(adj_closes[start..index], periods).last 18 | elsif start > 0 19 | output[index] = ((adj_close - output[index-1]) * k + output[index-1]) 20 | else 21 | output[index] = nil 22 | end 23 | end 24 | 25 | return output 26 | 27 | end 28 | 29 | end 30 | 31 | end -------------------------------------------------------------------------------- /lib/indicators/calculations/helper.rb: -------------------------------------------------------------------------------- 1 | module Indicators 2 | # Main helper methods 3 | class Helper 4 | 5 | # Error handling. 6 | class HelperException < StandardError ; end 7 | 8 | def self.validate_data data, column, parameters 9 | # If this is a hash, choose which column of values to use for calculations. 10 | if data.is_a?(Hash) 11 | valid_data = data[column] 12 | else 13 | valid_data = data 14 | end 15 | 16 | if valid_data.length < parameters 17 | raise HelperException, "Data point length (#{valid_data.length}) must be greater than or equal to the required indicator periods (#{parameters})." 18 | end 19 | return valid_data 20 | end 21 | 22 | # Indicators::Helper.get_parameters([12, 1, 1], 0, 15) 23 | def self.get_parameters parameters, i=0, default=0 24 | 25 | # Single parameter is to choose from. 26 | if parameters.is_a?(Integer) || parameters.is_a?(NilClass) 27 | 28 | # Set all other to default if only one integer is given instead of an array. 29 | return default if i != 0 30 | 31 | # Check if no parameters are specified at all, if so => set to default. 32 | # Parameters 15, 0, 0 are equal to 15 or 15, nil, nil. 33 | if parameters == nil || parameters == 0 34 | if default != 0 35 | return default 36 | else 37 | raise HelperException, 'There were no parameters specified and there is no default for it.' 38 | end 39 | else 40 | return parameters 41 | end 42 | # Multiple parameters to choose from. 43 | elsif parameters.is_a?(Array) 44 | # Map in case array is ["1", "2"] instead of [1, 2]. This usually happens when getting data from input forms. 45 | # Specify default as Float or Int, to indicate what is expected from the param. 46 | if default.is_a?(Integer) 47 | parameters = parameters.map(&:to_i) 48 | elsif default.is_a?(Float) 49 | parameters = parameters.map(&:to_f) 50 | end 51 | 52 | if parameters[i] == nil || parameters[i] == 0 53 | if default != 0 54 | return default 55 | else 56 | raise HelperException, 'There were no parameters specified and there is no default for it.' 57 | end 58 | else 59 | return parameters[i] 60 | end 61 | else 62 | raise HelperException, 'Parameters must be an integer, float, an array or nil.' 63 | end 64 | 65 | end 66 | 67 | end 68 | end 69 | 70 | # 71 | # Extra methods for mathematical calculations. 72 | module Enumerable 73 | 74 | def sum 75 | return self.inject(0){|accum, i| accum + i } 76 | end 77 | 78 | def mean 79 | return self.sum / self.length.to_f 80 | end 81 | 82 | def sample_variance 83 | m = self.mean 84 | sum = self.inject(0){|accum, i| accum + (i - m) ** 2 } 85 | return sum / (self.length - 1).to_f 86 | end 87 | 88 | def standard_deviation 89 | return Math.sqrt(self.sample_variance) 90 | end 91 | 92 | end -------------------------------------------------------------------------------- /lib/indicators/calculations/macd.rb: -------------------------------------------------------------------------------- 1 | module Indicators 2 | # 3 | # Moving Average Convergence Divergence 4 | class Macd 5 | 6 | # MACD Line: (12-day EMA - 26-day EMA) 7 | # Signal Line: 9-day EMA of MACD Line 8 | # MACD Histogram: MACD Line - Signal Line 9 | # Default MACD(12, 26, 9) 10 | def self.calculate data, parameters 11 | faster_periods = parameters[0] 12 | slower_periods = parameters[1] 13 | signal_periods = parameters[2] 14 | output = Array.new 15 | adj_closes = Indicators::Helper.validate_data(data, :adj_close, slower_periods+signal_periods-1) 16 | # puts "faster=#{faster_periods}, slower=#{slower_periods}, signal=#{signal_periods}" 17 | 18 | macd_line = [] 19 | 20 | adj_closes.each_with_index do |adj_close, index| 21 | if index+1 >= slower_periods 22 | # Calibrate me! Not sure why it doesn't accept from or from_faster. 23 | faster_ema = Indicators::Ema.calculate(adj_closes[0..index], faster_periods).last 24 | slower_ema = Indicators::Ema.calculate(adj_closes[0..index], slower_periods).last 25 | macd_line[index] = faster_ema - slower_ema 26 | if index+1 >= slower_periods+signal_periods 27 | signal_line = Indicators::Ema.calculate(macd_line[(-signal_periods)..index], signal_periods).last 28 | # Output is [MACD, Signal, MACD Hist] 29 | macd_histogram = macd_line[index] - signal_line 30 | output[index] = [macd_line[index], signal_line, macd_histogram] 31 | else 32 | output[index] = [macd_line[index], nil, nil] 33 | end 34 | else 35 | macd_line[index] = nil 36 | output[index] = nil 37 | end 38 | end 39 | 40 | return output 41 | 42 | end 43 | 44 | end 45 | end -------------------------------------------------------------------------------- /lib/indicators/calculations/rsi.rb: -------------------------------------------------------------------------------- 1 | module Indicators 2 | # 3 | # Relative Strength Index 4 | class Rsi 5 | 6 | # 100 7 | # RSI = 100 - -------- 8 | # 1 + RS 9 | # RS = Average Gain / Average Loss 10 | # First Average Gain = Sum of Gains over the past 14 periods / 14 11 | # First Average Loss = Sum of Losses over the past 14 periods / 14 12 | # Average Gain = [(previous Average Gain) x 13 + current Gain] / 14. 13 | # Average Loss = [(previous Average Loss) x 13 + current Loss] / 14. 14 | def self.calculate data, parameters 15 | periods = parameters 16 | output = Array.new 17 | adj_closes = Indicators::Helper.validate_data(data, :adj_close, periods) 18 | 19 | rs = Array.new 20 | average_gain = 0.0 21 | average_loss = 0.0 22 | adj_closes.each_with_index do |adj_close, index| 23 | if index >= periods 24 | if index == periods 25 | average_gain = gain_or_loss(adj_closes[0..index], :gain) / periods 26 | average_loss = gain_or_loss(adj_closes[0..index], :loss) / periods 27 | else 28 | difference = adj_close - adj_closes[index-1] 29 | if difference >= 0 30 | current_gain = difference 31 | current_loss = 0 32 | else 33 | current_gain = 0 34 | current_loss = difference.abs 35 | end 36 | average_gain = (average_gain * (periods-1) + current_gain) / periods 37 | average_loss = (average_loss * (periods-1) + current_loss) / periods 38 | end 39 | rs[index] = average_gain / average_loss 40 | output[index] = 100 - 100/(1+rs[index]) 41 | if average_gain == 0 42 | output[index] = 0 43 | elsif average_loss == 0 44 | output[index] = 100 45 | end 46 | else 47 | rs[index] = nil 48 | output[index] = nil 49 | end 50 | end 51 | 52 | return output 53 | 54 | end 55 | 56 | # 57 | # Helper methods for RSI 58 | def self.gain_or_loss data, type 59 | sum = 0.0 60 | first_value = nil 61 | data.each do |value| 62 | if first_value == nil 63 | first_value = value 64 | else 65 | if type == :gain 66 | if value > first_value 67 | sum += value - first_value 68 | end 69 | elsif type == :loss 70 | if value < first_value 71 | sum += first_value - value 72 | end 73 | end 74 | first_value = value 75 | end 76 | end 77 | return sum 78 | end 79 | 80 | end 81 | end -------------------------------------------------------------------------------- /lib/indicators/calculations/sma.rb: -------------------------------------------------------------------------------- 1 | module Indicators 2 | # 3 | # Simple Moving Average 4 | class Sma 5 | 6 | # SMA: (sum of closing prices for x period)/x 7 | def self.calculate data, parameters 8 | periods = parameters 9 | output = Array.new 10 | # Returns an array from the requested column and checks if there is enought data points. 11 | adj_closes = Indicators::Helper.validate_data(data, :adj_close, periods) 12 | 13 | adj_closes.each_with_index do |adj_close, index| 14 | start = index+1-periods 15 | if index+1 >= periods 16 | adj_closes_sum = adj_closes[start..index].sum 17 | output[index] = (adj_closes_sum/periods.to_f) 18 | else 19 | output[index] = nil 20 | end 21 | end 22 | return output 23 | 24 | end 25 | 26 | end 27 | end -------------------------------------------------------------------------------- /lib/indicators/calculations/sto.rb: -------------------------------------------------------------------------------- 1 | module Indicators 2 | # 3 | # Full Stochastic Oscillator 4 | class Sto 5 | 6 | # %K = (Current Close - Lowest Low)/(Highest High - Lowest Low) * 100 7 | # %D = 3-day SMA of %K 8 | # Lowest Low = lowest low for the look-back period 9 | # Highest High = highest high for the look-back period 10 | # %K is multiplied by 100 to move the decimal point two places 11 | # 12 | # Full %K = Fast %K smoothed with X-period SMA 13 | # Full %D = X-period SMA of Full %K 14 | # 15 | # Input 14, 3, 5 16 | # Returns [full %K, full %D] 17 | def self.calculate data, parameters 18 | k1_periods = parameters[0] 19 | k2_periods = parameters[1] 20 | d_periods = parameters[2] 21 | output = Array.new 22 | adj_closes = Indicators::Helper.validate_data(data, :adj_close, k1_periods) 23 | highs = Indicators::Helper.validate_data(data, :high, k1_periods) 24 | lows = Indicators::Helper.validate_data(data, :low, k1_periods) 25 | 26 | k1 = [] 27 | k2 = [] 28 | d = [] 29 | adj_closes.each_with_index do |adj_close, index| 30 | start = index+1-k1_periods 31 | if index+1 >= k1_periods 32 | k1[index] = (adj_close - lows[start..index].min) / (highs[start..index].max - lows[start..index].min) * 100 33 | if index+2 >= k1_periods + k2_periods 34 | k2[index] = Indicators::Sma.calculate(k1[(k1_periods-1)..index], k2_periods).last 35 | else 36 | k2[index] = nil 37 | end 38 | if index+3 >= k1_periods + k2_periods + d_periods 39 | d[index] = Indicators::Sma.calculate(k2[(k1_periods + k2_periods - 2)..index], d_periods).last 40 | else 41 | d[index] = nil 42 | end 43 | else 44 | k1[index] = nil 45 | end 46 | output[index] = [k1[index], k2[index], d[index]] 47 | end 48 | 49 | return output 50 | 51 | end 52 | 53 | end 54 | end -------------------------------------------------------------------------------- /lib/indicators/data.rb: -------------------------------------------------------------------------------- 1 | module Indicators 2 | class Data 3 | 4 | attr_reader :data, :results 5 | INDICATORS = [:sma, :ema, :bb, :macd, :rsi, :sto] 6 | # Error handling 7 | class DataException < StandardError ; end 8 | 9 | def initialize parameters 10 | @data = parameters 11 | 12 | # Check if data usable. 13 | if @data.nil? || @data.empty? 14 | raise DataException, 'There is no data to work on.' 15 | end 16 | unless @data.is_a?(Array) 17 | raise DataException, "Alien data. Given data must be an array (got #{@data.class})." 18 | end 19 | 20 | # Hacky, but a fast way to check if this is a dividends hash. 21 | raise DataException, 'Cannot use dividends values for technical analysis.' if @data.to_s.include?(':dividends') 22 | end 23 | 24 | def calc parameters 25 | # Check is parameters are usable. 26 | unless parameters.is_a?(Hash) 27 | raise DataException, 'Given parameters have to be a hash.' 28 | end 29 | 30 | # If not specified, set default :type to :sma. 31 | parameters[:type] = :sma if parameters[:type].nil? or parameters[:type].empty? 32 | 33 | # Check if there is such indicator type supported. 34 | case 35 | when INDICATORS.include?(parameters[:type]) then @results = Indicators::Main.new(@data, parameters) 36 | else 37 | raise DataException, "Invalid indicator type specified (#{parameters[:type]})." 38 | end 39 | end 40 | 41 | end 42 | end -------------------------------------------------------------------------------- /lib/indicators/main.rb: -------------------------------------------------------------------------------- 1 | module Indicators 2 | 3 | class Main 4 | 5 | attr_reader :output, :abbr, :params 6 | # Error handling. 7 | class MainException < StandardError ; end 8 | 9 | def initialize data, parameters 10 | type = parameters[:type] 11 | all_params = parameters[:params] 12 | @abbr = type.to_s.upcase 13 | case type 14 | when :sma 15 | @params = Indicators::Helper.get_parameters(all_params, 0, 20) 16 | when :ema 17 | @params = Indicators::Helper.get_parameters(all_params, 0, 20) 18 | when :bb 19 | @params = Array.new 20 | @params << Indicators::Helper.get_parameters(all_params, 0, 20) 21 | @params << Indicators::Helper.get_parameters(all_params, 1, 2.0) 22 | when :macd 23 | @params = Array.new 24 | @params << Indicators::Helper.get_parameters(all_params, 0, 12) 25 | @params << Indicators::Helper.get_parameters(all_params, 1, 26) 26 | @params << Indicators::Helper.get_parameters(all_params, 2, 9) 27 | when :rsi 28 | @params = Indicators::Helper.get_parameters(all_params, 0, 14) 29 | when :sto 30 | @params = Array.new 31 | @params << Indicators::Helper.get_parameters(all_params, 0, 14) 32 | @params << Indicators::Helper.get_parameters(all_params, 1, 3) 33 | @params << Indicators::Helper.get_parameters(all_params, 2, 3) 34 | end 35 | 36 | # This is a Securities gem hash. 37 | if data[0].is_a?(Hash) 38 | data = Indicators::Parser.parse_data(data) 39 | # Parser returns in {:date=>[2012.0, 2012.0, 2012.0], :open=>[409.4, 410.0, 414.95],} format 40 | else 41 | # Don't let calculation start on a standard array for few instruments cause it needs more specific data. 42 | if type == :sto 43 | raise MainException, 'You cannot calculate Stochastic Oscillator on array. Highs and lows are needed. Feel free Securities gem hash instead.' 44 | end 45 | end 46 | @output = case type 47 | when :sma then Indicators::Sma.calculate(data, @params) 48 | when :ema then Indicators::Ema.calculate(data, @params) 49 | when :bb then Indicators::Bb.calculate(data, @params) 50 | when :macd then Indicators::Macd.calculate(data, @params) 51 | when :rsi then Indicators::Rsi.calculate(data, @params) 52 | when :sto then Indicators::Sto.calculate(data, @params) 53 | end 54 | return @output 55 | end 56 | 57 | end 58 | end -------------------------------------------------------------------------------- /lib/indicators/parser.rb: -------------------------------------------------------------------------------- 1 | module Indicators 2 | 3 | # Class to parse a securities gem return hash. 4 | class Parser 5 | 6 | # Error handling. 7 | class ParserException < StandardError ; end 8 | 9 | def self.parse_data parameters 10 | 11 | usable_data = Hash.new 12 | transposed_hash = Hash.new 13 | # Such a hacky way to transpose an array. 14 | # FIXME: Now v.to_f converts date to float, it shouldn't. 15 | parameters.inject({}){|a, h| 16 | h.each_pair{|k,v| (a[k] ||= []) << v.to_f} 17 | transposed_hash = a 18 | } 19 | usable_data = transposed_hash 20 | # usable data is {:close => [1, 2, 3], :open => []} 21 | 22 | return usable_data 23 | end 24 | 25 | end 26 | end -------------------------------------------------------------------------------- /lib/indicators/version.rb: -------------------------------------------------------------------------------- 1 | module Indicators 2 | VERSION = "1.0.3" 3 | end 4 | -------------------------------------------------------------------------------- /spec/data_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Indicators::Data do 4 | 5 | describe ".new" do 6 | context "should raise an exception if parameter" do 7 | 8 | it "is nil or empty" do 9 | expect { Indicators::Data.new(nil) }.to raise_error('There is no data to work on.') 10 | expect { Indicators::Data.new('') }.to raise_error('There is no data to work on.') 11 | end 12 | it "is not an array" do 13 | expect { Indicators::Data.new('some string') }.to raise_error(Indicators::Data::DataException, /Alien data. Given data must be an array/) 14 | end 15 | it "contains dividends hash from Securities gem" do 16 | expect { Indicators::Data.new(Securities::Stock.new(:symbol => 'AAPL', :start_date => '2012-08-01', :end_date => '2012-08-10', :type => :dividends).output) }.to raise_error('Cannot use dividends values for technical analysis.') 17 | end 18 | end 19 | 20 | context "should not raise an exception if parameter" do 21 | 22 | it "is an array" do 23 | expect { Indicators::Data.new([1, 2, 3]) }.not_to raise_error 24 | end 25 | it "is a hash" do 26 | expect { Indicators::Data.new(Securities::Stock.new(:symbol => 'AAPL', :start_date => '2012-08-01', :end_date => '2012-08-03', :type => :daily).output) }.not_to raise_error 27 | end 28 | 29 | end 30 | end 31 | 32 | describe ".calc" do 33 | before :all do 34 | @my_data = Indicators::Data.new(Securities::Stock.new(:symbol => 'AAPL', :start_date => '2012-08-01', :end_date => '2012-08-20', :type => :daily).output) 35 | end 36 | 37 | context "should raise an exception if parameter" do 38 | it "is not a hash" do 39 | expect { @my_data.calc('some string') }.to raise_error('Given parameters have to be a hash.') 40 | end 41 | it ":type is invalid" do 42 | expect { @my_data.calc(:type => :invalid_type, :params => 5) }.to raise_error(Indicators::Data::DataException, /Invalid indicator type specified/) 43 | end 44 | end 45 | 46 | context "should not raise an exception if parameter" do 47 | it ":type is not specified (should default to :sma)" do 48 | expect { @my_data.calc(:params => 5) }.not_to raise_error 49 | # Can't get this test to work for some reason. 50 | # @my_data.calc(:params => 5).abbr.downcase.to_sym should eq(:sma) 51 | end 52 | it "good SMA params are specified" do 53 | expect { @my_data.calc(:type => :sma, :params => 5) }.not_to raise_error 54 | end 55 | it "good EMA params are specified" do 56 | expect { @my_data.calc(:type => :ema, :params => 5) }.not_to raise_error 57 | end 58 | it "good BB params are specified" do 59 | expect { @my_data.calc(:type => :bb, :params => [10, 2]) }.not_to raise_error 60 | end 61 | it "good MACD params are specified" do 62 | expect { @my_data.calc(:type => :macd, :params => [2, 5, 4]) }.not_to raise_error 63 | end 64 | it "good RSI params are specified" do 65 | expect { @my_data.calc(:type => :rsi, :params => 5) }.not_to raise_error 66 | end 67 | it "good STO params are specified" do 68 | expect { @my_data.calc(:type => :sto, :params => [3, 5, 4]) }.not_to raise_error 69 | end 70 | end 71 | end 72 | 73 | end -------------------------------------------------------------------------------- /spec/helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Indicators::Helper do 4 | 5 | before :all do 6 | @my_data = Indicators::Data.new([1, 2, 3, 4, 5]) 7 | end 8 | context "should raise an exception if" do 9 | it "there was not enough data for specified periods" do 10 | expect { @my_data.calc(:type => :sma, :params => 6) }.to raise_error('Data point length (5) must be greater than or equal to the required indicator periods (6).') 11 | end 12 | it "unknown type is given for parameters" do 13 | expect { @my_data.calc(:type => :sma, :params => 'unknown_type') }.to raise_error('Parameters must be an integer, float, an array or nil.') 14 | end 15 | end 16 | it "BB should accept float as multiplier" do 17 | @my_data.calc(:type => :bb, :params => [2, 2.5]).output[4][1].should > @my_data.calc(:type => :bb, :params => [2, 2]).output[4][1] 18 | @my_data.calc(:type => :bb, :params => [2, 2.5]).output[4][2].should < @my_data.calc(:type => :bb, :params => [2, 2]).output[4][2] 19 | end 20 | it "BB should not accept float as periods" do 21 | @my_data.calc(:type => :bb, :params => [2.9, 2.5]).output.should == @my_data.calc(:type => :bb, :params => [2, 2.5]).output 22 | @my_data.calc(:type => :bb, :params => [2.9, 2.5]).output[4][1].should > @my_data.calc(:type => :bb, :params => [2, 2]).output[4][1] 23 | end 24 | 25 | end -------------------------------------------------------------------------------- /spec/main_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Indicators::Main do 4 | 5 | context "should raise an exception if" do 6 | it "there was no data to work on" do 7 | @my_data = Indicators::Data.new([1, 2, 3, 4, 5]) 8 | expect { @my_data.calc(:type => :sto, :params => 2) }.to raise_error('You cannot calculate Stochastic Oscillator on array. Highs and lows are needed. Feel free Securities gem hash instead.') 9 | end 10 | end 11 | 12 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # Require this file using `require "spec_helper"` to ensure that it is only 4 | # loaded once. 5 | # 6 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 7 | $:.unshift File.dirname(__FILE__) + '/../lib' 8 | require 'indicators' 9 | 10 | RSpec.configure do |config| 11 | config.treat_symbols_as_metadata_keys_with_true_values = true 12 | config.run_all_when_everything_filtered = true 13 | config.filter_run :focus 14 | 15 | # Run specs in random order to surface order dependencies. If you find an 16 | # order dependency and want to debug it, you can fix the order by providing 17 | # the seed, which is printed after each run. 18 | # --seed 1234 19 | config.order = 'random' 20 | end 21 | --------------------------------------------------------------------------------