quantitative 0.1.9 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/Guardfile +1 -1
  4. data/Rakefile +6 -1
  5. data/lib/quant/attributes.rb +31 -43
  6. data/lib/quant/config.rb +8 -0
  7. data/lib/quant/experimental.rb +20 -0
  8. data/lib/quant/indicators/dominant_cycle_indicators.rb +10 -0
  9. data/lib/quant/indicators/dominant_cycles/acr.rb +101 -0
  10. data/lib/quant/indicators/dominant_cycles/band_pass.rb +80 -0
  11. data/lib/quant/indicators/dominant_cycles/differential.rb +19 -0
  12. data/lib/quant/indicators/dominant_cycles/dominant_cycle.rb +128 -0
  13. data/lib/quant/indicators/dominant_cycles/homodyne.rb +27 -0
  14. data/lib/quant/indicators/dominant_cycles/phase_accumulator.rb +59 -0
  15. data/lib/quant/indicators/indicator.rb +30 -8
  16. data/lib/quant/indicators/indicator_point.rb +12 -2
  17. data/lib/quant/indicators.rb +9 -2
  18. data/lib/quant/indicators_proxy.rb +0 -3
  19. data/lib/quant/indicators_sources.rb +1 -1
  20. data/lib/quant/interval.rb +6 -9
  21. data/lib/quant/mixins/filters.rb +5 -42
  22. data/lib/quant/mixins/functions.rb +7 -3
  23. data/lib/quant/mixins/high_pass_filters.rb +129 -0
  24. data/lib/quant/mixins/super_smoother.rb +18 -15
  25. data/lib/quant/mixins/universal_filters.rb +326 -0
  26. data/lib/quant/series.rb +1 -1
  27. data/lib/quant/statistics/correlation.rb +37 -0
  28. data/lib/quant/ticks/ohlc.rb +5 -4
  29. data/lib/quant/time_methods.rb +4 -0
  30. data/lib/quant/time_period.rb +13 -14
  31. data/lib/quant/version.rb +1 -1
  32. data/lib/quantitative.rb +1 -1
  33. metadata +13 -4
  34. data/lib/quant/indicators/ma.rb +0 -40
  35. data/lib/quant/mixins/high_pass_filter.rb +0 -54
@@ -7,13 +7,11 @@ module Quant
7
7
  include Mixins::Functions
8
8
  include Mixins::Filters
9
9
  include Mixins::MovingAverages
10
- # include Mixins::HilbertTransform
11
- # include Mixins::SuperSmoother
12
- # include Mixins::Stochastic
13
- # include Mixins::FisherTransform
14
- # include Mixins::HighPassFilter
10
+ include Mixins::HilbertTransform
11
+ include Mixins::SuperSmoother
12
+ include Mixins::Stochastic
13
+ include Mixins::FisherTransform
15
14
  # include Mixins::Direction
16
- # include Mixins::Filters
17
15
 
18
16
  attr_reader :source, :series
19
17
 
@@ -24,6 +22,30 @@ module Quant
24
22
  series.each { |tick| self << tick }
25
23
  end
26
24
 
25
+ def min_period
26
+ Quant.config.indicators.min_period
27
+ end
28
+
29
+ def max_period
30
+ Quant.config.indicators.max_period
31
+ end
32
+
33
+ def half_period
34
+ Quant.config.indicators.half_period
35
+ end
36
+
37
+ def micro_period
38
+ Quant.config.indicators.micro_period
39
+ end
40
+
41
+ def dominant_cycle_kind
42
+ Quant.config.indicators.dominant_cycle_kind
43
+ end
44
+
45
+ def pivot_kind
46
+ Quant.config.indicators.pivot_kind
47
+ end
48
+
27
49
  def ticks
28
50
  @points.keys
29
51
  end
@@ -45,7 +67,7 @@ module Quant
45
67
 
46
68
  def <<(tick)
47
69
  @t0 = tick
48
- @p0 = points_class.new(tick:, source:)
70
+ @p0 = points_class.new(indicator: self, tick:, source:)
49
71
  @points[tick] = @p0
50
72
 
51
73
  @p1 = values[-2] || @p0
@@ -64,7 +86,7 @@ module Quant
64
86
  end
65
87
 
66
88
  def inspect
67
- "#<#{self.class.name} symbol=#{series.symbol} source=#{source} #{ticks.size} ticks>"
89
+ "#<#{self.class.name} symbol=#{series.symbol} source=#{source} ticks=#{ticks.size}>"
68
90
  end
69
91
 
70
92
  def compute
@@ -4,19 +4,29 @@ module Quant
4
4
  class Indicators
5
5
  class IndicatorPoint
6
6
  include Quant::Attributes
7
+ extend Forwardable
7
8
 
8
- attr_reader :tick
9
+ attr_reader :indicator, :tick
9
10
 
10
11
  attribute :source, key: "src"
11
12
  attribute :input, key: "in"
12
13
 
13
- def initialize(tick:, source:)
14
+ def initialize(indicator:, tick:, source:)
15
+ @indicator = indicator
14
16
  @tick = tick
15
17
  @source = source
16
18
  @input = @tick.send(source)
17
19
  initialize_data_points
18
20
  end
19
21
 
22
+ def_delegator :indicator, :series
23
+ def_delegator :indicator, :min_period
24
+ def_delegator :indicator, :max_period
25
+ def_delegator :indicator, :half_period
26
+ def_delegator :indicator, :micro_period
27
+ def_delegator :indicator, :dominant_cycle_kind
28
+ def_delegator :indicator, :pivot_kind
29
+
20
30
  def initialize_data_points
21
31
  # No-Op - Override in subclass if needed.
22
32
  end
@@ -1,7 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "indicators_proxy"
3
4
  module Quant
4
- # TODO: build an Indicator registry so new indicators can be added and used outside those shipped with the library.
5
- class Indicators
5
+ # TODO: build an Indicator registry so new indicators can be added and
6
+ # used outside those shipped with the library.
7
+ class Indicators < IndicatorsProxy
8
+ def ping; indicator(Indicators::Ping) end
9
+
10
+ def dominant_cycles
11
+ @dominant_cycles ||= Indicators::DominantCycleIndicators.new(series:, source:)
12
+ end
6
13
  end
7
14
  end
@@ -54,8 +54,5 @@ module Quant
54
54
  def attach(name:, indicator_class:)
55
55
  define_singleton_method(name) { indicator(indicator_class) }
56
56
  end
57
-
58
- def ma; indicator(Indicators::Ma) end
59
- def ping; indicator(Indicators::Ping) end
60
57
  end
61
58
  end
@@ -12,7 +12,7 @@ module Quant
12
12
  end
13
13
 
14
14
  def oc2
15
- @indicator_sources[:oc2] ||= IndicatorsProxy.new(series: @series, source: :oc2)
15
+ @indicator_sources[:oc2] ||= Indicators.new(series: @series, source: :oc2)
16
16
  end
17
17
  end
18
18
  end
@@ -116,10 +116,6 @@ module Quant
116
116
  "1D" => :daily,
117
117
  }.freeze
118
118
 
119
- def self.all_resolutions
120
- RESOLUTIONS.keys
121
- end
122
-
123
119
  # Instantiates an Interval from a resolution. For example, TradingView uses resolutions
124
120
  # like "1", "3", "5", "15", "30", "60", "240", "D", "1D" to represent the duration of a
125
121
  # candlestick. +from_resolution+ translates resolutions to the appropriate {Quant::Interval}.
@@ -216,6 +212,11 @@ module Quant
216
212
  INTERVAL_DISTANCE.keys
217
213
  end
218
214
 
215
+ # Returns the full list of valid resolution Strings that can be used to instantiate an {Quant::Interval}.
216
+ def self.all_resolutions
217
+ RESOLUTIONS.keys
218
+ end
219
+
219
220
  # Computes the number of ticks from present to given timestamp.
220
221
  # If timestamp doesn't cover a full interval, it will be rounded up to 1
221
222
  # @example
@@ -230,7 +231,7 @@ module Quant
230
231
  end
231
232
 
232
233
  def self.ensure_valid_resolution!(resolution)
233
- return if RESOLUTIONS.keys.include? resolution
234
+ return if all_resolutions.include? resolution
234
235
 
235
236
  should_be_one_of = "Should be one of: (#{RESOLUTIONS.keys.join(", ")})"
236
237
  raise Errors::InvalidResolution, "resolution (#{resolution}) not a valid resolution. #{should_be_one_of}"
@@ -248,10 +249,6 @@ module Quant
248
249
  should_be_one_of = "Should be one of: (#{valid_intervals.join(", ")})"
249
250
  raise Errors::InvalidInterval, "interval (#{interval.inspect}) not a valid interval. #{should_be_one_of}"
250
251
  end
251
-
252
- def ensure_valid_resolution!(resolution)
253
- self.class.ensure_valid_resolution!(resolution)
254
- end
255
252
  end
256
253
  end
257
254
  # rubocop:enable Layout/HashAlignment
@@ -1,51 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "high_pass_filters"
4
+ require_relative "butterworth_filters"
5
+ require_relative "universal_filters"
3
6
  module Quant
4
7
  module Mixins
5
- # 1. All the common filters useful for traders have a transfer response
6
- # that can be written as a ratio of two polynomials.
7
- # 2. Lag is very important to traders. More complex filters can be
8
- # created using more input data, but more input data increases lag.
9
- # Sophisticated filters are not very useful for trading because they
10
- # incur too much lag.
11
- # 3. Filter transfer response can be viewed in the time domain and
12
- # the frequency domain with equal validity.
13
- # 4. Nonrecursive filters can have zeros in the transfer response, enabling
14
- # the complete cancellation of some selected frequency components.
15
- # 5. Nonrecursive filters having coefficients symmetrical about the
16
- # center of the filter will have a delay of half the degree of the
17
- # transfer response polynomial at all frequencies.
18
- # 6. Low-pass filters are smoothers because they attenuate the high-frequency
19
- # components of the input data.
20
- # 7. High-pass filters are detrenders because they attenuate the
21
- # low-frequency components of trends.
22
- # 8. Band-pass filters are both detrenders and smoothers because they
23
- # attenuate all but the desired frequency components.
24
- # 9. Filters provide an output only through their transfer response.
25
- # The transfer response is strictly a mathematical function, and
26
- # interpretations such as overbought, oversold, convergence, divergence,
27
- # and so on are not implied. The validity of such interpretations
28
- # must be made on the basis of statistics apart from the filter.
29
- # 10. The critical period of a filter output is the frequency at which
30
- # the output power of the filter is half the power of the input
31
- # wave at that frequency.
32
- # 11. A WMA has little or no redeeming virtue.
33
- # 12. A median filter is best used when the data contain impulsive noise
34
- # or when there are wild variations in the data. Smoothing volume
35
- # data is one example of a good application for a median filter.
36
- #
37
- # == Filter Coefficients forVariousTypes of Filters
38
- #
39
- # Filter Type b0 b1 b2 a0 a1 a2
40
- # EMA α 0 0 1 −(1−α) 0
41
- # Two-pole low-pass α**2 0 0 1 −2*(1-α) (1-α)**2
42
- # High-pass (1-α/2) -(1-α/2) 0 1 −(1−α) 0
43
- # Two-pole high-pass (1-α/2)**2 -2*(1-α/2)**2 (1-α/2)**2 1 -2*(1-α) (1-α)**2
44
- # Band-pass (1-σ)/2 0 -(1-σ)/2 1 -λ*(1+σ) σ
45
- # Band-stop (1+σ)/2 -2λ*(1+σ)/2 (1+σ)/2 1 -λ*(1+σ) σ
46
- #
47
8
  module Filters
9
+ include Mixins::HighPassFilters
48
10
  include Mixins::ButterworthFilters
11
+ include Mixins::UniversalFilters
49
12
  end
50
13
  end
51
14
  end
@@ -8,7 +8,7 @@ module Quant
8
8
  # k = 0.707 for two-pole high-pass filters
9
9
  # k = 1.414 for two-pole low-pass filters
10
10
  def period_to_alpha(period, k: 1.0)
11
- radians = deg2rad(k * 360 / period)
11
+ radians = deg2rad(k * 360 / period.to_f)
12
12
  cos = Math.cos(radians)
13
13
  sin = Math.sin(radians)
14
14
  (cos + sin - 1) / cos
@@ -48,8 +48,12 @@ module Quant
48
48
  dy2 = line2[1][1] - line1[1][1]
49
49
 
50
50
  d = dx1 * dx2 + dy1 * dy2
51
- l2 = (dx1**2 + dy1**2) * (dx2**2 + dy2**2)
52
- rad2deg Math.acos(d / Math.sqrt(l2))
51
+ l2 = ((dx1**2 + dy1**2) * (dx2**2 + dy2**2))
52
+
53
+ radians = d.to_f / Math.sqrt(l2)
54
+ value = rad2deg Math.acos(radians)
55
+
56
+ value.nan? ? 0.0 : value
53
57
  end
54
58
 
55
59
  # angle = acos(d/sqrt(l2))
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Mixins
5
+ # The following are high pass filters that are used to remove low frequency
6
+ # components from a time series. In simple terms, a high pass filter
7
+ # allows signals above a certain frequency (the cutoff frequency) to
8
+ # pass through relatively unaffected, while attenuating or blocking
9
+ # signals below that frequency.
10
+ #
11
+ # HighPass Filters are “detrenders” because they attenuate low frequency components
12
+ # One pole HighPass and SuperSmoother does not produce a zero mean because low
13
+ # frequency spectral dilation components are "leaking" through The one pole
14
+ # HighPass Filter response
15
+ #
16
+ # == Experimental
17
+ # Across the various texts and papers, Ehlers presents varying implementations
18
+ # of high-pass filters. I believe the two pole high-pass filter is the most
19
+ # consistently presented while the one pole high-pass filter has been presented
20
+ # in a few different ways. In some implementations, alpha is based on simple
21
+ # bars/lag while others use alpha based on phase/trigonometry. I have not been
22
+ # able to reconcile the differences and have not been able to find a definitive
23
+ # source for the correct implementation and do not know enough math to reason
24
+ # these out mathematically nor do I possess an advanced understanding of the
25
+ # fundamentals around digital signal processing. As such, the single-pole
26
+ # high-pass filters in this module are marked as experimental and may be incorrect.
27
+ module HighPassFilters
28
+ # A two-pole high-pass filter is a more advanced filtering technique
29
+ # used to remove low-frequency components from financial time series
30
+ # data, such as stock prices or market indices.
31
+ #
32
+ # Similar to a single-pole high-pass filter, a two-pole high-pass filter
33
+ # is designed to attenuate or eliminate slow-moving trends or macroeconomic
34
+ # effects from the data while preserving higher-frequency fluctuations.
35
+ # However, compared to the single-pole filter, the two-pole filter
36
+ # typically offers a steeper roll-off and better attenuation of lower
37
+ # frequencies, resulting in a more pronounced emphasis on short-term fluctuations.
38
+ def two_pole_high_pass_filter(source, period:, previous: :hp)
39
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
40
+ raise ArgumentError, "previous must be a Symbol" unless previous.is_a?(Symbol)
41
+
42
+ alpha = period_to_alpha(period, k: 0.707)
43
+
44
+ v1 = p0.send(source) - (2.0 * p1.send(source)) + p2.send(source)
45
+ v2 = p1.send(previous)
46
+ v3 = p2.send(previous)
47
+
48
+ a = v1 * (1 - (alpha * 0.5))**2
49
+ b = v2 * 2 * (1 - alpha)
50
+ c = v3 * (1 - alpha)**2
51
+
52
+ a + b - c
53
+ end
54
+
55
+ # A single-pole high-pass filter is used to filter out low-frequency
56
+ # components from financial time series data. This type of filter is
57
+ # commonly applied in signal processing techniques to remove noise or
58
+ # unwanted trends from the data while preserving higher-frequency fluctuations.
59
+ #
60
+ # A single-pole high-pass filter can be used to remove slow-moving trends
61
+ # or macroeconomic effects from the data, focusing instead on short-term
62
+ # fluctuations or high-frequency trading signals. By filtering out
63
+ # low-frequency components, traders aim to identify and exploit more
64
+ # immediate market opportunities, such as short-term price movements
65
+ # or momentum signals.
66
+ #
67
+ # The implementation of a single-pole high-pass filter in algorithmic
68
+ # trading typically involves applying a mathematical formula or algorithm
69
+ # to the historical price data of a financial instrument. This algorithm
70
+ # selectively attenuates or removes the low-frequency components of the
71
+ # data, leaving behind the higher-frequency fluctuations that traders
72
+ # are interested in analyzing for potential trading signals.
73
+ #
74
+ # Overall, single-pole high-pass filters in algorithmic trading are
75
+ # used as preprocessing steps to enhance the signal-to-noise ratio in
76
+ # financial data and to extract actionable trading signals from noisy
77
+ # or cluttered market data.
78
+ #
79
+ # == NOTES
80
+ # alpha = (Cosine(.707* 2 * PI / 48) + Sine (.707*360 / 48) - 1) / Cosine(.707*360 / 48);
81
+ # is the same as the following:
82
+ # radians = Math.sqrt(2) * Math::PI / period
83
+ # alpha = (Math.cos(radians) + Math.sin(radians) - 1) / Math.cos(radians)
84
+ def high_pass_filter(source, period:, previous: :hp)
85
+ Quant.experimental("This method is unproven and may be incorrect.")
86
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
87
+ raise ArgumentError, "previous must be a Symbol" unless previous.is_a?(Symbol)
88
+
89
+ radians = Math.sqrt(2) * Math::PI / period
90
+ a = Math.exp(-radians)
91
+ b = 2 * a * Math.cos(radians)
92
+
93
+ c2 = b
94
+ c3 = -a**2
95
+ c1 = (1 + c2 - c3) / 4
96
+
97
+ v0 = p0.send(source)
98
+ v1 = p1.send(source)
99
+ v2 = p2.send(source)
100
+ f1 = p1.send(previous)
101
+ f2 = p2.send(previous)
102
+
103
+ (c1 * (v0 - (2 * v1) + v2)) + (c2 * f1) + (c3 * f2)
104
+ end
105
+
106
+ # HPF = (1 − α/2)2 * (Price − 2 * Price[1] + Price[2]) + 2 * (1 − α) * HPF[1] − (1 − α)2 * HPF[2];
107
+ # High Pass Filter presented in Ehlers Cybernetic Analysis for Stocks and Futures Equation 2.7
108
+ def hpf2(source, period:, previous:)
109
+ Quant.experimental("This method is unproven and may be incorrect.")
110
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
111
+ raise ArgumentError, "previous must be a Symbol" unless previous.is_a?(Symbol)
112
+
113
+ alpha = period_to_alpha(period, k: 1.0)
114
+ v0 = p0.send(source)
115
+ v1 = p1.send(source)
116
+ v2 = p1.send(source)
117
+
118
+ f1 = p1.send(previous)
119
+ f2 = p2.send(previous)
120
+
121
+ c1 = (1 - alpha / 2)**2
122
+ c2 = 2 * (1 - alpha)
123
+ c3 = (1 - alpha)**2
124
+
125
+ (c1 * (v0 - (2 * v1) + v2)) + (c2 * f1) - (c3 * f2)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -6,39 +6,42 @@ module Quant
6
6
  def two_pole_super_smooth(source, period:, previous: :ss)
7
7
  raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
8
8
 
9
- radians = Math::PI * Math.sqrt(2) / period
9
+ radians = Math.sqrt(2) * Math::PI / period
10
10
  a1 = Math.exp(-radians)
11
11
 
12
- coef2 = 2.0r * a1 * Math.cos(radians)
13
- coef3 = -a1 * a1
14
- coef1 = 1.0 - coef2 - coef3
12
+ c3 = -a1**2
13
+ c2 = 2.0 * a1 * Math.cos(radians)
14
+ c1 = 1.0 - c2 - c3
15
15
 
16
- v0 = (p0.send(source) + p1.send(source)) / 2.0
17
- v1 = p2.send(previous)
18
- v2 = p3.send(previous)
19
- ((coef1 * v0) + (coef2 * v1) + (coef3 * v2)).to_f
16
+ v1 = (p0.send(source) + p1.send(source)) * 0.5
17
+ v2 = p2.send(previous)
18
+ v3 = p3.send(previous)
19
+
20
+ (c1 * v1) + (c2 * v2) + (c3 * v3)
20
21
  end
22
+
21
23
  alias super_smoother two_pole_super_smooth
22
24
  alias ss2p two_pole_super_smooth
23
25
 
24
26
  def three_pole_super_smooth(source, period:, previous: :ss)
25
27
  raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
26
28
 
27
- a1 = Math.exp(-Math::PI / period)
28
- b1 = 2 * a1 * Math.cos(Math::PI * Math.sqrt(3) / period)
29
+ radians = Math::PI / period
30
+ a1 = Math.exp(-radians)
31
+ b1 = 2 * a1 * Math.cos(Math.sqrt(3) * radians)
29
32
  c1 = a1**2
30
33
 
31
- coef2 = b1 + c1
32
- coef3 = -(c1 + b1 * c1)
33
- coef4 = c1**2
34
- coef1 = 1 - coef2 - coef3 - coef4
34
+ c4 = c1**2
35
+ c3 = -(c1 + b1 * c1)
36
+ c2 = b1 + c1
37
+ c1 = 1 - c2 - c3 - c4
35
38
 
36
39
  v0 = p0.send(source)
37
40
  v1 = p1.send(previous)
38
41
  v2 = p2.send(previous)
39
42
  v3 = p3.send(previous)
40
43
 
41
- (coef1 * v0) + (coef2 * v1) + (coef3 * v2) + (coef4 * v3)
44
+ (c1 * v0) + (c2 * v1) + (c3 * v2) + (c4 * v3)
42
45
  end
43
46
  alias ss3p three_pole_super_smooth
44
47
  end