Skip to content
This repository
Browse code

Merge branch 'time_series'

  • Loading branch information...
commit b00a07ff70a9565e65210bbb419a35afdda530fa 2 parents 99b742e + 6cadc1c
Claudio Bustos authored
1  .gitignore
... ... @@ -1,3 +1,4 @@
  1 +*.swp
1 2 *.rbc
2 3 coverage
3 4 *~
1  lib/statsample.rb
@@ -161,6 +161,7 @@ def self.create_has_library(library)
161 161 autoload(:Test, 'statsample/test')
162 162 autoload(:Factor, 'statsample/factor')
163 163 autoload(:Graph, 'statsample/graph')
  164 + autoload(:TimeSeries, 'statsample/tseries')
164 165
165 166
166 167 class << self
10 lib/statsample/matrix.rb
@@ -266,16 +266,16 @@ def get_new_name
266 266 # a=Matrix[[1.0, 0.3, 0.2],
267 267 # [0.3, 1.0, 0.5],
268 268 # [0.2, 0.5, 1.0]]
269   - # a.extends CovariateMatrix
270   - # a.labels=%w{a b c}
271   - # a.submatrix(%{c a}, %w{b})
  269 + # a.extend CovariateMatrix
  270 + # a.fields=%w{a b c}
  271 + # a.submatrix(%w{c a}, %w{b})
272 272 # => Matrix[[0.5],[0.3]]
273   - # a.submatrix(%{c a})
  273 + # a.submatrix(%w{c a})
274 274 # => Matrix[[1.0, 0.2] , [0.2, 1.0]]
275 275 def submatrix(rows,columns=nil)
276 276 raise ArgumentError, "rows shouldn't be empty" if rows.respond_to? :size and rows.size==0
277 277 columns||=rows
278   - # Convert all labels on index
  278 + # Convert all fields on index
279 279 row_index=rows.collect {|v|
280 280 r=v.is_a?(Numeric) ? v : fields_x.index(v)
281 281 raise "Index #{v} doesn't exists on matrix" if r.nil?
169 lib/statsample/tseries.rb
... ... @@ -0,0 +1,169 @@
  1 +module Statsample::TimeSeriesShorthands
  2 + # Creates a new Statsample::TimeSeries object
  3 + # Argument should be equal to TimeSeries.new
  4 + def to_time_series(*args)
  5 + Statsample::TimeSeries::TimeSeries.new(self, :scale, *args)
  6 + end
  7 +
  8 + alias :to_ts :to_time_series
  9 +end
  10 +
  11 +class Array
  12 + include Statsample::TimeSeriesShorthands
  13 +end
  14 +
  15 +module Statsample
  16 + module TimeSeries
  17 + # Collection of data indexed by time.
  18 + # The order goes from earliest to latest.
  19 + class TimeSeries < Statsample::Vector
  20 + # Calculates the autocorrelation coefficients of the series.
  21 + #
  22 + # The first element is always 1, since that is the correlation
  23 + # of the series with itself.
  24 + #
  25 + # Usage:
  26 + #
  27 + # ts = (1..100).map { rand }.to_time_series
  28 + #
  29 + # ts.acf # => array with first 21 autocorrelations
  30 + # ts.acf 3 # => array with first 3 autocorrelations
  31 + #
  32 + def acf maxlags = nil
  33 + maxlags ||= (10 * Math.log10(size)).to_i
  34 +
  35 + (0..maxlags).map do |i|
  36 + if i == 0
  37 + 1.0
  38 + else
  39 + m = self.mean
  40 +
  41 + # can't use Pearson coefficient since the mean for the lagged series should
  42 + # be the same as the regular series
  43 + ((self - m) * (self.lag(i) - m)).sum / self.variance_sample / (self.size - 1)
  44 + end
  45 + end
  46 + end
  47 +
  48 + # Lags the series by k periods.
  49 + #
  50 + # The convention is to set the oldest observations (the first ones
  51 + # in the series) to nil so that the size of the lagged series is the
  52 + # same as the original.
  53 + #
  54 + # Usage:
  55 + #
  56 + # ts = (1..10).map { rand }.to_time_series
  57 + # # => [0.69, 0.23, 0.44, 0.71, ...]
  58 + #
  59 + # ts.lag # => [nil, 0.69, 0.23, 0.44, ...]
  60 + # ts.lag 2 # => [nil, nil, 0.69, 0.23, ...]
  61 + #
  62 + def lag k = 1
  63 + return self if k == 0
  64 +
  65 + dup.tap do |lagged|
  66 + (lagged.size - 1).downto k do |i|
  67 + lagged[i] = lagged[i - k]
  68 + end
  69 +
  70 + (0...k).each do |i|
  71 + lagged[i] = nil
  72 + end
  73 + lagged.set_valid_data
  74 + end
  75 + end
  76 +
  77 + # Performs a first difference of the series.
  78 + #
  79 + # The convention is to set the oldest observations (the first ones
  80 + # in the series) to nil so that the size of the diffed series is the
  81 + # same as the original.
  82 + #
  83 + # Usage:
  84 + #
  85 + # ts = (1..10).map { rand }.to_ts
  86 + # # => [0.69, 0.23, 0.44, 0.71, ...]
  87 + #
  88 + # ts.diff # => [nil, -0.46, 0.21, 0.27, ...]
  89 + #
  90 + def diff
  91 + self - self.lag
  92 + end
  93 +
  94 + # Calculates a moving average of the series using the provided
  95 + # lookback argument. The lookback defaults to 10 periods.
  96 + #
  97 + # Usage:
  98 + #
  99 + # ts = (1..100).map { rand }.to_ts
  100 + # # => [0.69, 0.23, 0.44, 0.71, ...]
  101 + #
  102 + # # first 9 observations are nil
  103 + # ts.ma # => [ ... nil, 0.484... , 0.445... , 0.513 ... , ... ]
  104 + def ma n = 10
  105 + return mean if n >= size
  106 +
  107 + ([nil] * (n - 1) + (0..(size - n)).map do |i|
  108 + self[i...(i + n)].inject(&:+) / n
  109 + end).to_time_series
  110 + end
  111 +
  112 + # Calculates an exponential moving average of the series using a
  113 + # specified parameter. If wilder is false (the default) then the EMA
  114 + # uses a smoothing value of 2 / (n + 1), if it is true then it uses the
  115 + # Welles Wilder smoother of 1 / n.
  116 + #
  117 + # Warning for EMA usage: EMAs are unstable for small series, as they
  118 + # use a lot more than n observations to calculate. The series is stable
  119 + # if the size of the series is >= 3.45 * (n + 1)
  120 + #
  121 + # Usage:
  122 + #
  123 + # ts = (1..100).map { rand }.to_ts
  124 + # # => [0.69, 0.23, 0.44, 0.71, ...]
  125 + #
  126 + # # first 9 observations are nil
  127 + # ts.ema # => [ ... nil, 0.509... , 0.433..., ... ]
  128 + def ema n = 10, wilder = false
  129 + smoother = wilder ? 1.0 / n : 2.0 / (n + 1)
  130 +
  131 + # need to start everything from the first non-nil observation
  132 + start = self.data.index { |i| i != nil }
  133 +
  134 + # first n - 1 observations are nil
  135 + base = [nil] * (start + n - 1)
  136 +
  137 + # nth observation is just a moving average
  138 + base << self[start...(start + n)].inject(0.0) { |s, a| a.nil? ? s : s + a } / n
  139 +
  140 + (start + n).upto size - 1 do |i|
  141 + base << self[i] * smoother + (1 - smoother) * base.last
  142 + end
  143 +
  144 + base.to_time_series
  145 + end
  146 +
  147 + # Calculates the MACD (moving average convergence-divergence) of the time
  148 + # series - this is a comparison of a fast EMA with a slow EMA.
  149 + def macd fast = 12, slow = 26, signal = 9
  150 + series = ema(fast) - ema(slow)
  151 + [series, series.ema(signal)]
  152 + end
  153 +
  154 + # Borrow the operations from Vector, but convert to time series
  155 + def + series
  156 + super.to_a.to_ts
  157 + end
  158 +
  159 + def - series
  160 + super.to_a.to_ts
  161 + end
  162 +
  163 + def to_s
  164 + sprintf("Time Series(type:%s, n:%d)[%s]", @type.to_s, @data.size,
  165 + @data.collect{|d| d.nil? ? "nil":d}.join(","))
  166 + end
  167 + end
  168 + end
  169 +end
500 test/fixtures/stock_data.csv
... ... @@ -0,0 +1,500 @@
  1 +17.66
  2 +17.65
  3 +17.68
  4 +17.66
  5 +17.68
  6 +17.67
  7 +17.68
  8 +17.68
  9 +17.67
  10 +17.67
  11 +17.68
  12 +17.71
  13 +17.74
  14 +17.72
  15 +17.73
  16 +17.76
  17 +17.74
  18 +17.69
  19 +17.69
  20 +17.67
  21 +17.66
  22 +17.67
  23 +17.69
  24 +17.69
  25 +17.68
  26 +17.65
  27 +17.65
  28 +17.64
  29 +17.63
  30 +17.64
  31 +17.67
  32 +17.68
  33 +17.7
  34 +17.68
  35 +17.69
  36 +17.69
  37 +17.72
  38 +17.71
  39 +17.71
  40 +17.71
  41 +17.69
  42 +17.69
  43 +17.71
  44 +17.72
  45 +17.71
  46 +17.68
  47 +17.68
  48 +17.68
  49 +17.69
  50 +17.68
  51 +17.68
  52 +17.69
  53 +17.67
  54 +17.69
  55 +17.71
  56 +17.7
  57 +17.7
  58 +17.71
  59 +17.73
  60 +17.74
  61 +17.74
  62 +17.74
  63 +17.76
  64 +17.77
  65 +17.55
  66 +17.55
  67 +17.5
  68 +17.46
  69 +17.49
  70 +17.54
  71 +17.51
  72 +17.54
  73 +17.57
  74 +17.54
  75 +17.52
  76 +17.53
  77 +17.56
  78 +17.55
  79 +17.55
  80 +17.54
  81 +17.55
  82 +17.55
  83 +17.55
  84 +17.54
  85 +17.52
  86 +17.53
  87 +17.51
  88 +17.52
  89 +17.5
  90 +17.5
  91 +17.5
  92 +17.49
  93 +17.46
  94 +17.47
  95 +17.48
  96 +17.45
  97 +17.41
  98 +17.39
  99 +17.38
  100 +17.43
  101 +17.44
  102 +17.43
  103 +17.43
  104 +17.46
  105 +17.46
  106 +17.47
  107 +17.47
  108 +17.45
  109 +17.48
  110 +17.49
  111 +17.5
  112 +17.49
  113 +17.48
  114 +17.49
  115 +17.47
  116 +17.47
  117 +17.44
  118 +17.44
  119 +17.43
  120 +17.45
  121 +17.42
  122 +17.43
  123 +17.43
  124 +17.44
  125 +17.44
  126 +17.43
  127 +17.41
  128 +17.41
  129 +17.38
  130 +17.38
  131 +17.37
  132 +17.37
  133 +17.37
  134 +17.3
  135 +17.28
  136 +17.27
  137 +17.19
  138 +16.41
  139 +16.44
  140 +16.48
  141 +16.53
  142 +16.51
  143 +16.57
  144 +16.54
  145 +16.59
  146 +16.64
  147 +16.6
  148 +16.65
  149 +16.69
  150 +16.69
  151 +16.68
  152 +16.64
  153 +16.65
  154 +16.66
  155 +16.64
  156 +16.61
  157 +16.65
  158 +16.67
  159 +16.66
  160 +16.65
  161 +16.61
  162 +16.59
  163 +16.57
  164 +16.55
  165 +16.55
  166 +16.57
  167 +16.54
  168 +16.6
  169 +16.62
  170 +16.6
  171 +16.59
  172 +16.61
  173 +16.66
  174 +16.69
  175 +16.67
  176 +16.65
  177 +16.66
  178 +16.65
  179 +16.65
  180 +16.68
  181 +16.68
  182 +16.67
  183 +16.64
  184 +16.73
  185 +16.76
  186 +16.75
  187 +16.79
  188 +16.8
  189 +16.77
  190 +16.74
  191 +16.76
  192 +16.83
  193 +16.84
  194 +16.82
  195 +16.89
  196 +16.93
  197 +16.94
  198 +16.9
  199 +16.92
  200 +16.88
  201 +16.85
  202 +16.87
  203 +16.8
  204 +16.79
  205 +16.85
  206 +16.85
  207 +16.8
  208 +16.82
  209 +16.85
  210 +16.9
  211 +16.86
  212 +16.79
  213 +16.75
  214 +16.78
  215 +17.06
  216 +17.05
  217 +17.04
  218 +17.02
  219 +17.01
  220 +17.02
  221 +17.05
  222 +17.07
  223 +17.08
  224 +17.09
  225 +17.1
  226 +17.11
  227 +17.09
  228 +17.1
  229 +17.1
  230 +17.12
  231 +17.17
  232 +17.16
  233 +17.17
  234 +17.18
  235 +17.18
  236 +17.18
  237 +17.17
  238 +17.15
  239 +17.14
  240 +17.13
  241 +17.14
  242 +17.13
  243 +17.12
  244 +17.12
  245 +17.09
  246 +17.09
  247 +17.11
  248 +17.06
  249 +17.07
  250 +17.06
  251 +17.07
  252 +17.06
  253 +17.09
  254 +17.05
  255 +17.04
  256 +17.04
  257 +16.99
  258 +17
  259 +17.03
  260 +17
  261 +16.97
  262 +16.96
  263 +16.98
  264 +16.98
  265 +16.98
  266 +17.03
  267 +17
  268 +17
  269 +17
  270 +17.02
  271 +17
  272 +17.02
  273 +17.01
  274 +17.02
  275 +17.03
  276 +17.03
  277 +17.01
  278 +17.03
  279 +17.03
  280 +17.03
  281 +17.01
  282 +17.03
  283 +17.05
  284 +17.05
  285 +17.08
  286 +17.04
  287 +17.01
  288 +17.03
  289 +17.02
  290 +17.03
  291 +17.04
  292 +17.05
  293 +17.37
  294 +17.35
  295 +17.34
  296 +17.32
  297 +17.29
  298 +17.29
  299 +17.22
  300 +17.26
  301 +17.3
  302 +17.34
  303 +17.33
  304 +17.39
  305 +17.4
  306 +17.39
  307 +17.48
  308 +17.5
  309 +17.47
  310 +17.43
  311 +17.4
  312 +17.42
  313 +17.46
  314 +17.48
  315 +17.48
  316 +17.46
  317 +17.46
  318 +17.45
  319 +17.43
  320 +17.44
  321 +17.48
  322 +17.43
  323 +17.45
  324 +17.47
  325 +17.46
  326 +17.46
  327 +17.48
  328 +17.48
  329 +17.48
  330 +17.46
  331 +17.5
  332 +17.55
  333 +17.58
  334 +17.57
  335 +17.56
  336 +17.59
  337 +17.61
  338 +17.62
  339 +17.63
  340 +17.62
  341 +17.61
  342 +17.61
  343 +17.62
  344 +17.64
  345 +17.65
  346 +17.61
  347 +17.62
  348 +17.66
  349 +17.65
  350 +17.64
  351 +17.63
  352 +17.64
  353 +17.64
  354 +17.64
  355 +17.63
  356 +17.61
  357 +17.61
  358 +17.62
  359 +17.63
  360 +17.64
  361 +17.65
  362 +17.66
  363 +17.68
  364 +17.69
  365 +17.69
  366 +17.69
  367 +17.66
  368 +17.69
  369 +17.69
  370 +17.62
  371 +17.68
  372 +17.64
  373 +17.65
  374 +17.61
  375 +17.52
  376 +17.56
  377 +17.55
  378 +17.55
  379 +17.48
  380 +17.45
  381 +17.46
  382 +17.46
  383 +17.44
  384 +17.47
  385 +17.5
  386 +17.49
  387 +17.5
  388 +17.53
  389 +17.53
  390 +17.54
  391 +17.51
  392 +17.51
  393 +17.53
  394 +17.53
  395 +17.53
  396 +17.55
  397 +17.55
  398 +17.54
  399 +17.56
  400 +17.59
  401 +17.57
  402 +17.58
  403 +17.58
  404 +17.57
  405 +17.59
  406 +17.57
  407 +17.55
  408 +17.51
  409 +17.51
  410 +17.52
  411 +17.52
  412 +17.53
  413 +17.55
  414 +17.59
  415 +17.61
  416 +17.61
  417 +17.6
  418 +17.6
  419 +17.62
  420 +17.65
  421 +17.62
  422 +17.6
  423 +17.6
  424 +17.62
  425 +17.61
  426 +17.62
  427 +17.63
  428 +17.64
  429 +17.65
  430 +17.61
  431 +17.62
  432 +17.64
  433 +17.63
  434 +17.62
  435 +17.6
  436 +17.57
  437 +17.57
  438 +17.6
  439 +17.59
  440 +17.6
  441 +17.61
  442 +17.61
  443 +17.63
  444 +17.63
  445 +17.59
  446 +17.58
  447 +17.76
  448 +17.79
  449 +17.76
  450 +17.73
  451 +17.74
  452 +17.73
  453 +17.67
  454 +17.66
  455 +17.66
  456 +17.64
  457 +17.63
  458 +17.62
  459 +17.61
  460 +17.6
  461 +17.61
  462 +17.61
  463 +17.6
  464 +17.6
  465 +17.64
  466 +17.65
  467 +17.65
  468 +17.63
  469 +17.61
  470 +17.6
  471 +17.63
  472 +17.63
  473 +17.62
  474 +17.63
  475 +17.64
  476 +17.62
  477 +17.63
  478 +17.65
  479 +17.64
  480 +17.6
  481 +17.59
  482 +17.59
  483 +17.58
  484 +17.58
  485 +17.6
  486 +17.6
  487 +17.6
  488 +17.6
  489 +17.6
  490 +17.58
  491 +17.59
  492 +17.6
  493 +17.6
  494 +17.6
  495 +17.59
  496 +17.59
  497 +17.58
  498 +17.58
  499 +17.65
  500 +17.65
94 test/test_tseries.rb
... ... @@ -0,0 +1,94 @@
  1 +require(File.expand_path(File.dirname(__FILE__)+'/helpers_tests.rb'))
  2 +
  3 +class StatsampleTestTimeSeries < MiniTest::Unit::TestCase
  4 + include Statsample::Shorthand
  5 +
  6 + # All calculations are compared to the output of the equivalent function in R
  7 +
  8 + def setup
  9 + # daily closes of iShares XIU on the TSX
  10 + @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,
  11 + 16.86, 16.86, 16.56, 16.36, 16.66, 16.77], :scale
  12 + end
  13 +
  14 + def test_acf
  15 + acf = @xiu.acf
  16 +
  17 + assert_equal 14, acf.length
  18 +
  19 + # test the first few autocorrelations
  20 + assert_in_delta 1.0, acf[0], 0.0001
  21 + assert_in_delta 0.852, acf[1], 0.001
  22 + assert_in_delta 0.669, acf[2], 0.001
  23 + assert_in_delta 0.486, acf[3], 0.001
  24 + end
  25 +
  26 + def test_lag
  27 + assert_in_delta 16.66, @xiu.lag[@xiu.size - 1], 0.001
  28 + assert_in_delta 16.36, @xiu.lag(2)[@xiu.size - 1], 0.001
  29 + end
  30 +
  31 + def test_delta
  32 + diff = @xiu.diff
  33 +
  34 + assert_in_delta 0.11, diff[@xiu.size - 1], 0.001
  35 + assert_in_delta 0.30, diff[@xiu.size - 2], 0.001
  36 + assert_in_delta -0.20, diff[@xiu.size - 3], 0.001
  37 + end
  38 +
  39 + def test_ma
  40 + # test default
  41 + ma10 = @xiu.ma
  42 +
  43 + assert_in_delta ma10[-1], 16.897, 0.001
  44 + assert_in_delta ma10[-5], 17.233, 0.001
  45 + assert_in_delta ma10[-10], 17.587, 0.001
  46 +
  47 + # test with a different lookback period
  48 + ma5 = @xiu.ma 5
  49 +
  50 + assert_in_delta ma5[-1], 16.642, 0.001
  51 + assert_in_delta ma5[-10], 17.434, 0.001
  52 + assert_in_delta ma5[-15], 17.74, 0.001
  53 + end
  54 +
  55 + def test_ema
  56 + # test default
  57 + ema10 = @xiu.ema
  58 +
  59 + assert_in_delta ema10[-1], 16.87187, 0.00001
  60 + assert_in_delta ema10[-5], 17.19187, 0.00001
  61 + assert_in_delta ema10[-10], 17.54918, 0.00001
  62 +
  63 + # test with a different lookback period
  64 + ema5 = @xiu.ema 5
  65 +
  66 + assert_in_delta ema5[-1], 16.71299, 0.0001
  67 + assert_in_delta ema5[-10], 17.49079, 0.0001
  68 + assert_in_delta ema5[-15], 17.70067, 0.0001
  69 +
  70 + # test with a different smoother
  71 + ema_w = @xiu.ema 10, true
  72 +
  73 + assert_in_delta ema_w[-1], 17.08044, 0.00001
  74 + assert_in_delta ema_w[-5], 17.33219, 0.00001
  75 + assert_in_delta ema_w[-10], 17.55810, 0.00001
  76 + end
  77 +
  78 + def test_macd
  79 + # MACD uses a lot more data than the other ones, so we need a bigger vector
  80 + data = File.readlines(File.dirname(__FILE__) + "/fixtures/stock_data.csv").map(&:to_f).to_time_series
  81 +
  82 + macd, signal = data.macd
  83 +
  84 + # check the MACD
  85 + assert_in_delta 3.12e-4, macd[-1], 1e-6
  86 + assert_in_delta -1.07e-2, macd[-10], 1e-4
  87 + assert_in_delta -5.65e-3, macd[-20], 1e-5
  88 +
  89 + # check the signal
  90 + assert_in_delta -0.00628, signal[-1], 1e-5
  91 + assert_in_delta -0.00971, signal[-10], 1e-5
  92 + assert_in_delta -0.00338, signal[-20], 1e-5
  93 + end
  94 +end

0 comments on commit b00a07f

Please sign in to comment.
Something went wrong with that request. Please try again.