Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'time_series'

  • Loading branch information...
commit b00a07ff70a9565e65210bbb419a35afdda530fa 2 parents 99b742e + 6cadc1c
Claudio Bustos authored
1  .gitignore
View
@@ -1,3 +1,4 @@
+*.swp
*.rbc
coverage
*~
1  lib/statsample.rb
View
@@ -161,6 +161,7 @@ def self.create_has_library(library)
autoload(:Test, 'statsample/test')
autoload(:Factor, 'statsample/factor')
autoload(:Graph, 'statsample/graph')
+ autoload(:TimeSeries, 'statsample/tseries')
class << self
10 lib/statsample/matrix.rb
View
@@ -266,16 +266,16 @@ def get_new_name
# a=Matrix[[1.0, 0.3, 0.2],
# [0.3, 1.0, 0.5],
# [0.2, 0.5, 1.0]]
- # a.extends CovariateMatrix
- # a.labels=%w{a b c}
- # a.submatrix(%{c a}, %w{b})
+ # a.extend CovariateMatrix
+ # a.fields=%w{a b c}
+ # a.submatrix(%w{c a}, %w{b})
# => Matrix[[0.5],[0.3]]
- # a.submatrix(%{c a})
+ # a.submatrix(%w{c a})
# => Matrix[[1.0, 0.2] , [0.2, 1.0]]
def submatrix(rows,columns=nil)
raise ArgumentError, "rows shouldn't be empty" if rows.respond_to? :size and rows.size==0
columns||=rows
- # Convert all labels on index
+ # Convert all fields on index
row_index=rows.collect {|v|
r=v.is_a?(Numeric) ? v : fields_x.index(v)
raise "Index #{v} doesn't exists on matrix" if r.nil?
169 lib/statsample/tseries.rb
View
@@ -0,0 +1,169 @@
+module Statsample::TimeSeriesShorthands
+ # Creates a new Statsample::TimeSeries object
+ # Argument should be equal to TimeSeries.new
+ def to_time_series(*args)
+ Statsample::TimeSeries::TimeSeries.new(self, :scale, *args)
+ end
+
+ alias :to_ts :to_time_series
+end
+
+class Array
+ include Statsample::TimeSeriesShorthands
+end
+
+module Statsample
+ module TimeSeries
+ # Collection of data indexed by time.
+ # The order goes from earliest to latest.
+ class TimeSeries < Statsample::Vector
+ # Calculates the autocorrelation coefficients of the series.
+ #
+ # The first element is always 1, since that is the correlation
+ # of the series with itself.
+ #
+ # Usage:
+ #
+ # ts = (1..100).map { rand }.to_time_series
+ #
+ # ts.acf # => array with first 21 autocorrelations
+ # ts.acf 3 # => array with first 3 autocorrelations
+ #
+ def acf maxlags = nil
+ maxlags ||= (10 * Math.log10(size)).to_i
+
+ (0..maxlags).map do |i|
+ if i == 0
+ 1.0
+ else
+ m = self.mean
+
+ # can't use Pearson coefficient since the mean for the lagged series should
+ # be the same as the regular series
+ ((self - m) * (self.lag(i) - m)).sum / self.variance_sample / (self.size - 1)
+ end
+ end
+ end
+
+ # Lags the series by k periods.
+ #
+ # The convention is to set the oldest observations (the first ones
+ # in the series) to nil so that the size of the lagged series is the
+ # same as the original.
+ #
+ # Usage:
+ #
+ # ts = (1..10).map { rand }.to_time_series
+ # # => [0.69, 0.23, 0.44, 0.71, ...]
+ #
+ # ts.lag # => [nil, 0.69, 0.23, 0.44, ...]
+ # ts.lag 2 # => [nil, nil, 0.69, 0.23, ...]
+ #
+ def lag k = 1
+ return self if k == 0
+
+ dup.tap do |lagged|
+ (lagged.size - 1).downto k do |i|
+ lagged[i] = lagged[i - k]
+ end
+
+ (0...k).each do |i|
+ lagged[i] = nil
+ end
+ lagged.set_valid_data
+ end
+ end
+
+ # Performs a first difference of the series.
+ #
+ # The convention is to set the oldest observations (the first ones
+ # in the series) to nil so that the size of the diffed series is the
+ # same as the original.
+ #
+ # Usage:
+ #
+ # ts = (1..10).map { rand }.to_ts
+ # # => [0.69, 0.23, 0.44, 0.71, ...]
+ #
+ # ts.diff # => [nil, -0.46, 0.21, 0.27, ...]
+ #
+ def diff
+ self - self.lag
+ end
+
+ # Calculates a moving average of the series using the provided
+ # lookback argument. The lookback defaults to 10 periods.
+ #
+ # Usage:
+ #
+ # ts = (1..100).map { rand }.to_ts
+ # # => [0.69, 0.23, 0.44, 0.71, ...]
+ #
+ # # first 9 observations are nil
+ # ts.ma # => [ ... nil, 0.484... , 0.445... , 0.513 ... , ... ]
+ def ma n = 10
+ return mean if n >= size
+
+ ([nil] * (n - 1) + (0..(size - n)).map do |i|
+ self[i...(i + n)].inject(&:+) / n
+ end).to_time_series
+ end
+
+ # Calculates an exponential moving average of the series using a
+ # specified parameter. If wilder is false (the default) then the EMA
+ # uses a smoothing value of 2 / (n + 1), if it is true then it uses the
+ # Welles Wilder smoother of 1 / n.
+ #
+ # Warning for EMA usage: EMAs are unstable for small series, as they
+ # use a lot more than n observations to calculate. The series is stable
+ # if the size of the series is >= 3.45 * (n + 1)
+ #
+ # Usage:
+ #
+ # ts = (1..100).map { rand }.to_ts
+ # # => [0.69, 0.23, 0.44, 0.71, ...]
+ #
+ # # first 9 observations are nil
+ # ts.ema # => [ ... nil, 0.509... , 0.433..., ... ]
+ def ema n = 10, wilder = false
+ smoother = wilder ? 1.0 / n : 2.0 / (n + 1)
+
+ # need to start everything from the first non-nil observation
+ start = self.data.index { |i| i != nil }
+
+ # first n - 1 observations are nil
+ base = [nil] * (start + n - 1)
+
+ # nth observation is just a moving average
+ base << self[start...(start + n)].inject(0.0) { |s, a| a.nil? ? s : s + a } / n
+
+ (start + n).upto size - 1 do |i|
+ base << self[i] * smoother + (1 - smoother) * base.last
+ end
+
+ base.to_time_series
+ end
+
+ # Calculates the MACD (moving average convergence-divergence) of the time
+ # series - this is a comparison of a fast EMA with a slow EMA.
+ def macd fast = 12, slow = 26, signal = 9
+ series = ema(fast) - ema(slow)
+ [series, series.ema(signal)]
+ end
+
+ # Borrow the operations from Vector, but convert to time series
+ def + series
+ super.to_a.to_ts
+ end
+
+ def - series
+ super.to_a.to_ts
+ end
+
+ def to_s
+ sprintf("Time Series(type:%s, n:%d)[%s]", @type.to_s, @data.size,
+ @data.collect{|d| d.nil? ? "nil":d}.join(","))
+ end
+ end
+ end
+end
500 test/fixtures/stock_data.csv
View
@@ -0,0 +1,500 @@
+17.66
+17.65
+17.68
+17.66
+17.68
+17.67
+17.68
+17.68
+17.67
+17.67
+17.68
+17.71
+17.74
+17.72
+17.73
+17.76
+17.74
+17.69
+17.69
+17.67
+17.66
+17.67
+17.69
+17.69
+17.68
+17.65
+17.65
+17.64
+17.63
+17.64
+17.67
+17.68
+17.7
+17.68
+17.69
+17.69
+17.72
+17.71
+17.71
+17.71
+17.69
+17.69
+17.71
+17.72
+17.71
+17.68
+17.68
+17.68
+17.69
+17.68
+17.68
+17.69
+17.67
+17.69
+17.71
+17.7
+17.7
+17.71
+17.73
+17.74
+17.74
+17.74
+17.76
+17.77
+17.55
+17.55
+17.5
+17.46
+17.49
+17.54
+17.51
+17.54
+17.57
+17.54
+17.52
+17.53
+17.56
+17.55
+17.55
+17.54
+17.55
+17.55
+17.55
+17.54
+17.52
+17.53
+17.51
+17.52
+17.5
+17.5
+17.5
+17.49
+17.46
+17.47
+17.48
+17.45
+17.41
+17.39
+17.38
+17.43
+17.44
+17.43
+17.43
+17.46
+17.46
+17.47
+17.47
+17.45
+17.48
+17.49
+17.5
+17.49
+17.48
+17.49
+17.47
+17.47
+17.44
+17.44
+17.43
+17.45
+17.42
+17.43
+17.43
+17.44
+17.44
+17.43
+17.41
+17.41
+17.38
+17.38
+17.37
+17.37
+17.37
+17.3
+17.28
+17.27
+17.19
+16.41
+16.44
+16.48
+16.53
+16.51
+16.57
+16.54
+16.59
+16.64
+16.6
+16.65
+16.69
+16.69
+16.68
+16.64
+16.65
+16.66
+16.64
+16.61
+16.65
+16.67
+16.66
+16.65
+16.61
+16.59
+16.57
+16.55
+16.55
+16.57
+16.54
+16.6
+16.62
+16.6
+16.59
+16.61
+16.66
+16.69
+16.67
+16.65
+16.66
+16.65
+16.65
+16.68
+16.68
+16.67
+16.64
+16.73
+16.76
+16.75
+16.79
+16.8
+16.77
+16.74
+16.76
+16.83
+16.84
+16.82
+16.89
+16.93
+16.94
+16.9
+16.92
+16.88
+16.85
+16.87
+16.8
+16.79
+16.85
+16.85
+16.8
+16.82
+16.85
+16.9
+16.86
+16.79
+16.75
+16.78
+17.06
+17.05
+17.04
+17.02
+17.01
+17.02
+17.05
+17.07
+17.08
+17.09
+17.1
+17.11
+17.09
+17.1
+17.1
+17.12
+17.17
+17.16
+17.17
+17.18
+17.18
+17.18
+17.17
+17.15
+17.14
+17.13
+17.14
+17.13
+17.12
+17.12
+17.09
+17.09
+17.11
+17.06
+17.07
+17.06
+17.07
+17.06
+17.09
+17.05
+17.04
+17.04
+16.99
+17
+17.03
+17
+16.97
+16.96
+16.98
+16.98
+16.98
+17.03
+17
+17
+17
+17.02
+17
+17.02
+17.01
+17.02
+17.03
+17.03
+17.01
+17.03
+17.03
+17.03
+17.01
+17.03
+17.05
+17.05
+17.08
+17.04
+17.01
+17.03
+17.02
+17.03
+17.04
+17.05
+17.37
+17.35
+17.34
+17.32
+17.29
+17.29
+17.22
+17.26
+17.3
+17.34
+17.33
+17.39
+17.4
+17.39
+17.48
+17.5
+17.47
+17.43
+17.4
+17.42
+17.46
+17.48
+17.48
+17.46
+17.46
+17.45
+17.43
+17.44
+17.48
+17.43
+17.45
+17.47
+17.46
+17.46
+17.48
+17.48
+17.48
+17.46
+17.5
+17.55
+17.58
+17.57
+17.56
+17.59
+17.61
+17.62
+17.63
+17.62
+17.61
+17.61
+17.62
+17.64
+17.65
+17.61
+17.62
+17.66
+17.65
+17.64
+17.63
+17.64
+17.64
+17.64
+17.63
+17.61
+17.61
+17.62
+17.63
+17.64
+17.65
+17.66
+17.68
+17.69
+17.69
+17.69
+17.66
+17.69
+17.69
+17.62
+17.68
+17.64
+17.65
+17.61
+17.52
+17.56
+17.55
+17.55
+17.48
+17.45
+17.46
+17.46
+17.44
+17.47
+17.5
+17.49
+17.5
+17.53
+17.53
+17.54
+17.51
+17.51
+17.53
+17.53
+17.53
+17.55
+17.55
+17.54
+17.56
+17.59
+17.57
+17.58
+17.58
+17.57
+17.59
+17.57
+17.55
+17.51
+17.51
+17.52
+17.52
+17.53
+17.55
+17.59
+17.61
+17.61
+17.6
+17.6
+17.62
+17.65
+17.62
+17.6
+17.6
+17.62
+17.61
+17.62
+17.63
+17.64
+17.65
+17.61
+17.62
+17.64
+17.63
+17.62
+17.6
+17.57
+17.57
+17.6
+17.59
+17.6
+17.61
+17.61
+17.63
+17.63
+17.59
+17.58
+17.76
+17.79
+17.76
+17.73
+17.74
+17.73
+17.67
+17.66
+17.66
+17.64
+17.63
+17.62
+17.61
+17.6
+17.61
+17.61
+17.6
+17.6
+17.64
+17.65
+17.65
+17.63
+17.61
+17.6
+17.63
+17.63
+17.62
+17.63
+17.64
+17.62
+17.63
+17.65
+17.64
+17.6
+17.59
+17.59
+17.58
+17.58
+17.6
+17.6
+17.6
+17.6
+17.6
+17.58
+17.59
+17.6
+17.6
+17.6
+17.59
+17.59
+17.58
+17.58
+17.65
+17.65
94 test/test_tseries.rb
View
@@ -0,0 +1,94 @@
+require(File.expand_path(File.dirname(__FILE__)+'/helpers_tests.rb'))
+
+class StatsampleTestTimeSeries < MiniTest::Unit::TestCase
+ include Statsample::Shorthand
+
+ # All calculations are compared to the output of the equivalent function in R
+
+ def setup
+ # daily closes of iShares XIU on the TSX
+ @xiu = Statsample::TimeSeries::TimeSeries.new [17.28, 17.45, 17.84, 17.74, 17.82, 17.85, 17.36, 17.3, 17.56, 17.49, 17.46, 17.4, 17.03, 17.01,
+ 16.86, 16.86, 16.56, 16.36, 16.66, 16.77], :scale
+ end
+
+ def test_acf
+ acf = @xiu.acf
+
+ assert_equal 14, acf.length
+
+ # test the first few autocorrelations
+ assert_in_delta 1.0, acf[0], 0.0001
+ assert_in_delta 0.852, acf[1], 0.001
+ assert_in_delta 0.669, acf[2], 0.001
+ assert_in_delta 0.486, acf[3], 0.001
+ end
+
+ def test_lag
+ assert_in_delta 16.66, @xiu.lag[@xiu.size - 1], 0.001
+ assert_in_delta 16.36, @xiu.lag(2)[@xiu.size - 1], 0.001
+ end
+
+ def test_delta
+ diff = @xiu.diff
+
+ assert_in_delta 0.11, diff[@xiu.size - 1], 0.001
+ assert_in_delta 0.30, diff[@xiu.size - 2], 0.001
+ assert_in_delta -0.20, diff[@xiu.size - 3], 0.001
+ end
+
+ def test_ma
+ # test default
+ ma10 = @xiu.ma
+
+ assert_in_delta ma10[-1], 16.897, 0.001
+ assert_in_delta ma10[-5], 17.233, 0.001
+ assert_in_delta ma10[-10], 17.587, 0.001
+
+ # test with a different lookback period
+ ma5 = @xiu.ma 5
+
+ assert_in_delta ma5[-1], 16.642, 0.001
+ assert_in_delta ma5[-10], 17.434, 0.001
+ assert_in_delta ma5[-15], 17.74, 0.001
+ end
+
+ def test_ema
+ # test default
+ ema10 = @xiu.ema
+
+ assert_in_delta ema10[-1], 16.87187, 0.00001
+ assert_in_delta ema10[-5], 17.19187, 0.00001
+ assert_in_delta ema10[-10], 17.54918, 0.00001
+
+ # test with a different lookback period
+ ema5 = @xiu.ema 5
+
+ assert_in_delta ema5[-1], 16.71299, 0.0001
+ assert_in_delta ema5[-10], 17.49079, 0.0001
+ assert_in_delta ema5[-15], 17.70067, 0.0001
+
+ # test with a different smoother
+ ema_w = @xiu.ema 10, true
+
+ assert_in_delta ema_w[-1], 17.08044, 0.00001
+ assert_in_delta ema_w[-5], 17.33219, 0.00001
+ assert_in_delta ema_w[-10], 17.55810, 0.00001
+ end
+
+ def test_macd
+ # MACD uses a lot more data than the other ones, so we need a bigger vector
+ data = File.readlines(File.dirname(__FILE__) + "/fixtures/stock_data.csv").map(&:to_f).to_time_series
+
+ macd, signal = data.macd
+
+ # check the MACD
+ assert_in_delta 3.12e-4, macd[-1], 1e-6
+ assert_in_delta -1.07e-2, macd[-10], 1e-4
+ assert_in_delta -5.65e-3, macd[-20], 1e-5
+
+ # check the signal
+ assert_in_delta -0.00628, signal[-1], 1e-5
+ assert_in_delta -0.00971, signal[-10], 1e-5
+ assert_in_delta -0.00338, signal[-20], 1e-5
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.