quantitative 0.1.9 → 0.1.10

Sign up to get free protection for your applications and to get access to all the features.
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