quantitative 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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