quantitative 0.1.9 → 0.1.10

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7ddfcfd200564c3ce9764a7ba635635c56649c6681ac6d76b93390734da4fc9
4
- data.tar.gz: 04d93778a91c74ee4c3459009f4990644121974b77079d79a3816c5f63a63f6c
3
+ metadata.gz: 50dde4546ee2c241a00695bf5928fb76c92a7323e8b1a2d5d0090db74bbebee5
4
+ data.tar.gz: f6432d642ac5111cd4fe0cb6b041ada60af39ba41033195c949b0126184ae180
5
5
  SHA512:
6
- metadata.gz: 8facaa3efaef73c00f98f03b4482b876cf9a6155f46fb56f1c0d3534c996b6c33fefa067ee07ef8f326e76dfbdd41b0fa7d04f349eef49add179444bbe0d9db6
7
- data.tar.gz: 3fa7bb3d2e34cd3f2ebd718105ab20ffb60e044c45fb5c3909a14cdd1d2876288ae23d4eb7c4211f35890ddd250772068f1c0e0463db71d1c84c490a06edc2d8
6
+ metadata.gz: d48777cee5009e48b5d3ef6c531029fdaa8c9629b37223ad919aacd53c29caf13258d7cddc28413788b61ec086af0abde606996cdac3c7012565ec2cea5b6e77
7
+ data.tar.gz: 8c9fa61a0f4b4c21b890c6895d4e74ba870fb92336cd30bb822e13bad5e49e9ee8d4b45e639171a73ab8c04e1756088e79979231a557a41ad9effa8ee1660e6b
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quantitative (0.1.9)
4
+ quantitative (0.1.10)
5
5
  oj (~> 3.10)
6
6
 
7
7
  GEM
data/Guardfile CHANGED
@@ -1,6 +1,6 @@
1
1
  guard :rspec, cmd: "rspec" do
2
2
  watch(%r{^spec/.+_spec\.rb$})
3
- watch(%r{^spec/lib/**/.+_spec\.rb$})
3
+ watch(%r{^spec/lib/.+_spec\.rb$})
4
4
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
5
5
  watch("spec/spec_helper.rb") { "spec" }
6
6
  end
data/Rakefile CHANGED
@@ -38,4 +38,9 @@ namespace :gem do
38
38
  task release: [:build, :tag] do
39
39
  sh "gem push quantitative-#{Quant::VERSION}.gem"
40
40
  end
41
- end
41
+
42
+ desc "push #{Quant::VERSION} to rubygems.org"
43
+ task push: [:build] do
44
+ sh "gem push quantitative-#{Quant::VERSION}.gem"
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Experimental
5
+ def self.tracker
6
+ @tracker ||= {}
7
+ end
8
+ end
9
+
10
+ def self.experimental(message)
11
+ return if defined?(RSpec)
12
+ return if Experimental.tracker[caller.first]
13
+
14
+ Experimental.tracker[caller.first] = message
15
+
16
+ calling_method = caller.first.scan(/`([^']*)/)[0][0]
17
+ full_message = "EXPERIMENTAL: #{calling_method.inspect}: #{message}\nsource location: #{caller.first}"
18
+ puts full_message
19
+ end
20
+ end
@@ -64,7 +64,7 @@ module Quant
64
64
  end
65
65
 
66
66
  def inspect
67
- "#<#{self.class.name} symbol=#{series.symbol} source=#{source} #{ticks.size} ticks>"
67
+ "#<#{self.class.name} symbol=#{series.symbol} source=#{source} ticks=#{ticks.size}>"
68
68
  end
69
69
 
70
70
  def compute
@@ -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))
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Quant
4
4
  module Mixins
5
- module HighPassFilter
5
+ module HighPassFilters
6
6
  # HighPass Filters are “detrenders” because they attenuate low frequency components
7
7
  # One pole HighPass and SuperSmoother does not produce a zero mean because low
8
8
  # frequency spectral dilation components are “leaking” through The one pole
@@ -32,8 +32,10 @@ module Quant
32
32
  # is the same as the following:
33
33
  # radians = Math.sqrt(2) * Math::PI / period
34
34
  # alpha = (Math.cos(radians) + Math.sin(radians) - 1) / Math.cos(radians)
35
- def high_pass_filter(source, period)
36
- v0 = source.is_a?(Symbol) ? p0.send(source) : source
35
+ def high_pass_filter(source, period:, previous: :hp)
36
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
37
+
38
+ v0 = p0.send(source)
37
39
  return v0 if p3 == p0
38
40
 
39
41
  v1 = p1.send(source)
@@ -9,7 +9,7 @@ module Quant
9
9
  radians = Math::PI * Math.sqrt(2) / period
10
10
  a1 = Math.exp(-radians)
11
11
 
12
- coef2 = 2.0r * a1 * Math.cos(radians)
12
+ coef2 = 2.0 * a1 * Math.cos(radians)
13
13
  coef3 = -a1 * a1
14
14
  coef1 = 1.0 - coef2 - coef3
15
15
 
@@ -0,0 +1,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Mixins
5
+ # Source: Ehlers, J. F. (2013). Cycle Analytics for Traders:
6
+ # Advanced Technical Trading Concepts. John Wiley & Sons.
7
+ #
8
+ # == Universal Filters
9
+ # Ehlers devoted a chapter in his book to generalizing the algorithms
10
+ # for the most common filters used in trading. The universal filters
11
+ # makes an attempt to translate that work into working code. However,
12
+ # Ehler write up contained some typos and incomplete treatment of the
13
+ # subject matter, and as a non-mathematician, I am not sure if I have
14
+ # translated the formulas correctly. So far, I have not been able to
15
+ # prove correctness of the universal EMA vs. the optimzed EMA, but
16
+ # the others are still unproven and Ehlers' many papers over the year
17
+ # tend to change implementation details, too.
18
+ #
19
+ # == Ehlers' Notes on Generalized Filters
20
+ # 1. All the common filters useful for traders have a transfer response
21
+ # that can be written as a ratio of two polynomials.
22
+ # 2. Lag is very important to traders. More complex filters can be
23
+ # created using more input data, but more input data increases lag.
24
+ # Sophisticated filters are not very useful for trading because they
25
+ # incur too much lag.
26
+ # 3. Filter transfer response can be viewed in the time domain and
27
+ # the frequency domain with equal validity.
28
+ # 4. Nonrecursive filters can have zeros in the transfer response, enabling
29
+ # the complete cancellation of some selected frequency components.
30
+ # 5. Nonrecursive filters having coefficients symmetrical about the
31
+ # center of the filter will have a delay of half the degree of the
32
+ # transfer response polynomial at all frequencies.
33
+ # 6. Low-pass filters are smoothers because they attenuate the high-frequency
34
+ # components of the input data.
35
+ # 7. High-pass filters are detrenders because they attenuate the
36
+ # low-frequency components of trends.
37
+ # 8. Band-pass filters are both detrenders and smoothers because they
38
+ # attenuate all but the desired frequency components.
39
+ # 9. Filters provide an output only through their transfer response.
40
+ # The transfer response is strictly a mathematical function, and
41
+ # interpretations such as overbought, oversold, convergence, divergence,
42
+ # and so on are not implied. The validity of such interpretations
43
+ # must be made on the basis of statistics apart from the filter.
44
+ # 10. The critical period of a filter output is the frequency at which
45
+ # the output power of the filter is half the power of the input
46
+ # wave at that frequency.
47
+ # 11. A WMA has little or no redeeming virtue.
48
+ # 12. A median filter is best used when the data contain impulsive noise
49
+ # or when there are wild variations in the data. Smoothing volume
50
+ # data is one example of a good application for a median filter.
51
+ #
52
+ # == Filter Coefficients forVariousTypes of Filters
53
+ #
54
+ # Filter Type b0 b1 b2 a1 a2
55
+ # EMA α 0 0 −(1−α) 0
56
+ # Two-pole low-pass α**2 0 0 −2*(1-α) (1-α)**2
57
+ # High-pass (1-α/2) -(1-α/2) 0 −(1−α) 0
58
+ # Two-pole high-pass (1-α/2)**2 -2*(1-α/2)**2 (1-α/2)**2 -2*(1-α) (1-α)**2
59
+ # Band-pass (1-σ)/2 0 -(1-σ)/2 -λ*(1+σ) σ
60
+ # Band-stop (1+σ)/2 -2λ*(1+σ)/2 (1+σ)/2 -λ*(1+σ) σ
61
+ #
62
+ module UniversalFilters
63
+ K = {
64
+ single_pole: 1.0,
65
+ two_pole_high_pass: Math.sqrt(2) * 0.5,
66
+ two_pole_low_pass: Math.sqrt(2)
67
+ }.freeze
68
+
69
+ # The universal filter is a generalization of the common filters. The other
70
+ # algorithms in this module are derived from this one.
71
+ def universal_filter(source, previous:, b0:, b1:, b2:, a1:, a2:)
72
+ b0 * p0.send(source) +
73
+ b1 * p1.send(source) +
74
+ b2 * p2.send(source) -
75
+ a1 * p1.send(previous) -
76
+ a2 * p2.send(previous)
77
+ end
78
+
79
+ # The EMA is optimized in the {Quant::Mixins::ExponentialMovingAverage} module
80
+ # and its correctness is proven with this particular implementation.
81
+ def universal_ema(source, previous:, period:)
82
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
83
+ raise ArgumentError, ":previous must be a Symbol" unless previous.is_a?(Symbol)
84
+
85
+ alpha = bars_to_alpha(period)
86
+ b0 = alpha
87
+ b1 = 0
88
+ b2 = 0
89
+ a1 = -(1 - alpha)
90
+ a2 = 0
91
+
92
+ universal_filter(source, previous:, b0:, b1:, b2:, a1:, a2:)
93
+ end
94
+
95
+ # The two-pole low-pass filter can serve several purposes:
96
+ #
97
+ # 1. Noise Reduction: Stock price data often contains high-frequency
98
+ # fluctuations or noise due to market volatility, algorithmic
99
+ # trading, or other factors. By applying a low-pass filter, you can
100
+ # smooth out these fluctuations, making it easier to identify
101
+ # underlying trends or patterns in the price action.
102
+ # 2. Trend Identification: Low-pass filtering can help in identifying
103
+ # the underlying trend in stock price movements by removing short-term
104
+ # fluctuations and emphasizing longer-term movements. This can be
105
+ # useful for trend-following strategies or identifying potential
106
+ # trend reversals.
107
+ # 3. Signal Smoothing: Filtering can help in removing erratic or
108
+ # spurious movements in the price data, providing a clearer and
109
+ # more consistent representation of the overall price action.
110
+ # 4. Highlighting Structural Changes: Filtering can make structural
111
+ # changes in the price action more apparent by reducing noise and
112
+ # focusing on significant movements. This can be useful for detecting
113
+ # shifts in market sentiment or the emergence of new trends.
114
+ # 5. Trade Signal Generation: Smoothed price data from a low-pass filter
115
+ # can be used as input for trading strategies, such as moving
116
+ # average-based strategies or momentum strategies, where identifying
117
+ # trends or momentum is crucial.
118
+ def universal_two_pole_low_pass(source, previous:, period:)
119
+ Quant.experimental("This method is unproven and may be incorrect.")
120
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
121
+ raise ArgumentError, ":previous must be a Symbol" unless previous.is_a?(Symbol)
122
+
123
+ alpha = period_to_alpha(period, k: K[:two_pole_low_pass] )
124
+ b0 = alpha**2
125
+ b1 = 0
126
+ b2 = 0
127
+ a1 = -2.0 * (1.0 - alpha)
128
+ a2 = (1.0 - alpha)**2
129
+
130
+ universal_filter(source, previous:, b0:, b1:, b2:, a1:, a2:)
131
+ end
132
+
133
+ # A single-pole low-pass filter, also known as a first-order low-pass
134
+ # filter, has a simpler response compared to a two-pole low-pass filter.
135
+ # It attenuates higher-frequency components of the signal while allowing
136
+ # lower-frequency components to pass through, but it does so with a
137
+ # gentler roll-off compared to higher-order filters.
138
+ #
139
+ # Here's what a single-pole low-pass filter typically does to stock
140
+ # price action data:
141
+ # 1. Noise Reduction: Similar to a two-pole low-pass filter, a single-pole
142
+ # filter can help in reducing high-frequency noise in stock price data,
143
+ # smoothing out rapid fluctuations caused by market volatility or other factors.
144
+ # 2. Trend Identification: It can aid in identifying trends by smoothing
145
+ # out short-term fluctuations, making the underlying trend more apparent.
146
+ # However, compared to higher-order filters, it may not provide as
147
+ # sharp or accurate trend signals.
148
+ # 3. Signal Smoothing: Single-pole filters provide a basic level of signal
149
+ # smoothing, which can help in removing minor fluctuations and emphasizing
150
+ # larger movements in the price action. This can make the data easier to
151
+ # interpret and analyze.
152
+ # 4. Delay and Responsiveness: Single-pole filters introduce less delay
153
+ # compared to higher-order filters, making them more responsive to changes
154
+ # in the input signal. However, this responsiveness comes at the cost of a
155
+ # less aggressive attenuation of high-frequency noise.
156
+ # 5. Simple Filtering: Single-pole filters are computationally efficient and
157
+ # easy to implement, making them suitable for real-time processing and
158
+ # applications where simplicity is preferred.
159
+ #
160
+ # Overall, while a single-pole low-pass filter can still be effective for
161
+ # noise reduction and basic trend identification in stock price action data,
162
+ # it may not offer the same level of precision and robustness as higher-order
163
+ # filters. The choice between single-pole and higher-order filters depends on
164
+ # the specific requirements of the analysis and the trade-offs between
165
+ # responsiveness and noise attenuation.
166
+ def universal_one_pole_low_pass(source, previous:, period:)
167
+ Quant.experimental("This method is unproven and may be incorrect.")
168
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
169
+ raise ArgumentError, ":previous must be a Symbol" unless previous.is_a?(Symbol)
170
+
171
+ alpha = period_to_alpha(period, k: K[:single_pole])
172
+ b0 = alpha
173
+ b1 = 0
174
+ b2 = 0
175
+ a1 = -(1 - alpha)
176
+ a2 = 0
177
+
178
+ universal_filter(source, previous:, b0:, b1:, b2:, a1:, a2:)
179
+ end
180
+
181
+ # A single-pole high-pass filter, also known as a first-order high-pass
182
+ # filter, attenuates low-frequency components of a signal while allowing
183
+ # higher-frequency components to pass through. In the context of processing
184
+ # stock price action data, applying a single-pole high-pass filter has
185
+ # several potential effects:
186
+ #
187
+ # 1. Removal of Low-Frequency Trends: Similar to higher-order high-pass
188
+ # filters, a single-pole high-pass filter removes or attenuates slow-moving
189
+ # components of the price action data, such as long-term trends. This can
190
+ # help in focusing on shorter-term fluctuations and identifying short-term
191
+ # trading opportunities.
192
+ # 2. Noise Amplification: As with higher-order high-pass filters, a single-pole
193
+ # high-pass filter can amplify high-frequency noise present in the data,
194
+ # especially if the cutoff frequency is set too low. This noise amplification
195
+ # can make it challenging to analyze the data accurately, particularly if the
196
+ # noise overwhelms the signal of interest.
197
+ # 3. Emphasis on Short-Term Variations: By removing low-frequency components, a
198
+ # single-pole high-pass filter highlights short-term variations and rapid
199
+ # movements in the price action data. This can be beneficial for traders or
200
+ # analysts who are primarily interested in short-term price dynamics or
201
+ # intraday trading opportunities.
202
+ # 4. Simple Filtering: Single-pole filters are computationally efficient and
203
+ # straightforward to implement, making them suitable for real-time processing
204
+ # and applications where simplicity is preferred. However, they may not offer
205
+ # the same level of noise attenuation and signal preservation as higher-order
206
+ # filters.
207
+ # 5. Enhanced Responsiveness: Single-pole high-pass filters offer relatively high
208
+ # responsiveness to changes in the input signal, reflecting recent price movements
209
+ # quickly. This responsiveness can be advantageous for certain trading strategies
210
+ # that rely on timely identification of market events or short-term trends.
211
+ #
212
+ # Overall, applying a single-pole high-pass filter to stock price action data can
213
+ # help in removing low-frequency trends and focusing on short-term variations and
214
+ # rapid movements. However, it's essential to carefully select the cutoff frequency
215
+ # to balance noise attenuation with signal preservation and to consider the potential
216
+ # trade-offs between simplicity and filtering effectiveness.
217
+ def universal_one_pole_high_pass(source, previous:, period:)
218
+ Quant.experimental("This method is unproven and may be incorrect.")
219
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
220
+ raise ArgumentError, ":previous must be a Symbol" unless previous.is_a?(Symbol)
221
+
222
+ alpha = period_to_alpha(period, k: K[:single_pole])
223
+ b0 = (1 - alpha / 2)
224
+ b1 = -(1 - alpha / 2)
225
+ b2 = 0
226
+ a1 = -(1 - alpha)
227
+ a2 = 0
228
+
229
+ universal_filter(source, previous:, b0:, b1:, b2:, a1:, a2:)
230
+ end
231
+
232
+ # A two-pole high-pass filter, also known as a second-order high-pass
233
+ # filter, attenuates low-frequency components of a signal while allowing
234
+ # higher-frequency components to pass through. In the context of
235
+ # processing stock price action data, applying a two-pole high-pass
236
+ # filter has several potential effects:
237
+ #
238
+ # 1. Removal of Low-Frequency Trends: High-pass filtering can remove
239
+ # or attenuate long-term trends or slow-moving components of the
240
+ # price action data. This can be useful for focusing on shorter-term
241
+ # fluctuations or identifying short-term trading opportunities.
242
+ # 2. Noise Attenuation: By suppressing low-frequency components, a
243
+ # high-pass filter can help in reducing the impact of slow-moving
244
+ # noise or irrelevant signals in the data. This can improve the
245
+ # clarity and interpretability of the price action data.
246
+ # 3. Noise Amplification: High-pass filters can amplify high-frequency
247
+ # noise present in the data, particularly if the cutoff frequency is
248
+ # set too low. This noise amplification can make it challenging to
249
+ # analyze the data accurately, especially if the noise overwhelms
250
+ # the signal of interest.
251
+ # 4. Emphasis on Short-Term Variations: By removing low-frequency
252
+ # components, a high-pass filter highlights short-term variations
253
+ # and rapid movements in the price action data. This can be beneficial
254
+ # for traders or analysts who are primarily interested in short-term
255
+ # price dynamics.
256
+ # 5. Enhanced Responsiveness: Compared to low-pass filters, high-pass
257
+ # filters typically offer greater responsiveness to changes in the
258
+ # input signal. This means that high-pass filtered data can reflect
259
+ # recent price movements more quickly, which may be advantageous for
260
+ # certain trading strategies.
261
+ # 6. Identification of Market Events: High-pass filtering can help in
262
+ # identifying specific market events or anomalies that occur on
263
+ # shorter time scales, such as intraday price spikes or volatility
264
+ # clusters.
265
+ #
266
+ # Overall, applying a two-pole high-pass filter to stock price action
267
+ # data can help in focusing on short-term variations and removing
268
+ # long-term trends or slow-moving components. However, it's essential
269
+ # to carefully select the cutoff frequency to balance noise attenuation
270
+ # with signal preservation, as excessive noise amplification can degrade
271
+ # the quality of the analysis. Additionally, high-pass filtering may
272
+ # not be suitable for all trading or analysis purposes, and its effects
273
+ # should be evaluated in the context of specific goals and strategies.
274
+ def universal_two_pole_high_pass(source, previous:, period:)
275
+ Quant.experimental("This method is unproven and may be incorrect.")
276
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
277
+ raise ArgumentError, ":previous must be a Symbol" unless previous.is_a?(Symbol)
278
+
279
+ alpha = period_to_alpha(period, k: K[:two_pole_high_pass])
280
+ b0 = (1 - alpha / 2)**2
281
+ b1 = -2 * (1 - alpha / 2)**2
282
+ b2 = (1 - alpha / 2)**2
283
+ a1 = -2 * (1 - alpha)
284
+ a2 = (1 - alpha)**2
285
+
286
+ universal_filter(source, previous:, b0:, b1:, b2:, a1:, a2:)
287
+ end
288
+
289
+ # Band-pass filters are both detrenders and smoothers because they
290
+ # attenuate all but the desired frequency components.
291
+ # NOTE: Ehlers' book contains a typo in the formula for the band-pass
292
+ # filter. I am not sure what the correct formulation is, so
293
+ # this is a best guess for how, left for further investigation.
294
+ def universal_band_pass(source, previous:, period:)
295
+ Quant.experimental("This method is unproven and may be incorrect.")
296
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
297
+ raise ArgumentError, ":previous must be a Symbol" unless previous.is_a?(Symbol)
298
+
299
+ lambda = deg2rad(360.0 / period)
300
+ gamma = Math.cos(lambda)
301
+ sigma = 1 / gamma - Math.sqrt(1 / gamma**2 - 1)
302
+
303
+ b0 = (1 - sigma) * 0.5
304
+ b1 = 0
305
+ b2 = -(1 - sigma) * 0.5
306
+ a1 = -lambda * (1 + sigma)
307
+ a2 = sigma
308
+
309
+ universal_filter(source, previous:, b0:, b1:, b2:, a1:, a2:)
310
+ end
311
+ end
312
+ end
313
+ end
data/lib/quant/series.rb CHANGED
@@ -114,7 +114,7 @@ module Quant
114
114
 
115
115
  def to_h
116
116
  { "symbol" => symbol,
117
- "interval" => interval,
117
+ "interval" => interval.to_s,
118
118
  "ticks" => ticks.map(&:to_h) }
119
119
  end
120
120
 
@@ -82,9 +82,10 @@ module Quant
82
82
  # A value of 0.0 means no change.
83
83
  # @return [Float]
84
84
  def daily_price_change
85
- ((open_price / close_price) - 1.0)
86
- rescue ZeroDivisionError
87
- 0.0
85
+ return open_price.zero? ? 0.0 : -1.0 if close_price.zero?
86
+ return 0.0 if open_price == close_price
87
+
88
+ (open_price / close_price) - 1.0
88
89
  end
89
90
 
90
91
  # Calculates the absolute change from the open_price to the close_price, divided by the average of the
@@ -93,7 +94,7 @@ module Quant
93
94
  # This method is useful for comparing the volatility of different assets.
94
95
  # @return [Float]
95
96
  def daily_price_change_ratio
96
- @daily_price_change_ratio ||= ((open_price - close_price) / oc2).abs
97
+ (open_price - close_price).abs / oc2
97
98
  end
98
99
 
99
100
  # Set the #green? property to true when the close_price is greater than or equal to the open_price.
@@ -41,6 +41,10 @@ module Quant
41
41
  case value
42
42
  when Time
43
43
  value.utc
44
+ when DateTime
45
+ Time.new(value.year, value.month, value.day, value.hour, value.minute, value.second).utc
46
+ when Date
47
+ Time.utc(value.year, value.month, value.day, 0, 0, 0)
44
48
  when Integer
45
49
  Time.at(value).utc
46
50
  when String
@@ -36,7 +36,7 @@ module Quant
36
36
  def validate_bounds!
37
37
  return if lower_bound? || upper_bound?
38
38
 
39
- raise "TimePeriod cannot be unbounded at start_at and end_at"
39
+ raise "TimePeriod cannot be unbound at start_at and end_at"
40
40
  end
41
41
 
42
42
  def cover?(value)
@@ -48,11 +48,19 @@ module Quant
48
48
  end
49
49
 
50
50
  def lower_bound?
51
- !!@start_at
51
+ !lower_unbound?
52
+ end
53
+
54
+ def lower_unbound?
55
+ @start_at.nil?
56
+ end
57
+
58
+ def upper_unbound?
59
+ @end_at.nil?
52
60
  end
53
61
 
54
62
  def upper_bound?
55
- !!@end_at
63
+ !upper_unbound?
56
64
  end
57
65
 
58
66
  def end_at
@@ -66,17 +74,8 @@ module Quant
66
74
  def ==(other)
67
75
  return false unless other.is_a?(TimePeriod)
68
76
 
69
- if lower_bound?
70
- other.lower_bound? && start_at == other.start_at
71
- elsif upper_bound?
72
- oher.upper_bound? && end_at == other.end_at
73
- else
74
- [start_at, end_at] == [other.start_at, other.end_at]
75
- end
76
- end
77
-
78
- def eql?(other)
79
- self == other
77
+ [lower_bound?, upper_bound?, start_at, end_at] ==
78
+ [other.lower_bound?, other.upper_bound?, other.start_at, other.end_at]
80
79
  end
81
80
 
82
81
  def to_h
data/lib/quant/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quant
4
- VERSION = "0.1.9"
4
+ VERSION = "0.1.10"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quantitative
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Lang
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-28 00:00:00.000000000 Z
11
+ date: 2024-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -49,6 +49,7 @@ files:
49
49
  - lib/quant/attributes.rb
50
50
  - lib/quant/config.rb
51
51
  - lib/quant/errors.rb
52
+ - lib/quant/experimental.rb
52
53
  - lib/quant/indicators.rb
53
54
  - lib/quant/indicators/indicator.rb
54
55
  - lib/quant/indicators/indicator_point.rb
@@ -63,12 +64,13 @@ files:
63
64
  - lib/quant/mixins/filters.rb
64
65
  - lib/quant/mixins/fisher_transform.rb
65
66
  - lib/quant/mixins/functions.rb
66
- - lib/quant/mixins/high_pass_filter.rb
67
+ - lib/quant/mixins/high_pass_filters.rb
67
68
  - lib/quant/mixins/hilbert_transform.rb
68
69
  - lib/quant/mixins/moving_averages.rb
69
70
  - lib/quant/mixins/simple_moving_average.rb
70
71
  - lib/quant/mixins/stochastic.rb
71
72
  - lib/quant/mixins/super_smoother.rb
73
+ - lib/quant/mixins/universal_filters.rb
72
74
  - lib/quant/mixins/weighted_moving_average.rb
73
75
  - lib/quant/refinements/array.rb
74
76
  - lib/quant/series.rb