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 +4 -4
- data/Gemfile.lock +1 -1
- data/Guardfile +1 -1
- data/Rakefile +6 -1
- data/lib/quant/experimental.rb +20 -0
- data/lib/quant/indicators/indicator.rb +1 -1
- data/lib/quant/interval.rb +6 -9
- data/lib/quant/mixins/filters.rb +5 -42
- data/lib/quant/mixins/functions.rb +7 -3
- data/lib/quant/mixins/{high_pass_filter.rb → high_pass_filters.rb} +5 -3
- data/lib/quant/mixins/super_smoother.rb +1 -1
- data/lib/quant/mixins/universal_filters.rb +313 -0
- data/lib/quant/series.rb +1 -1
- data/lib/quant/ticks/ohlc.rb +5 -4
- data/lib/quant/time_methods.rb +4 -0
- data/lib/quant/time_period.rb +13 -14
- data/lib/quant/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 50dde4546ee2c241a00695bf5928fb76c92a7323e8b1a2d5d0090db74bbebee5
|
4
|
+
data.tar.gz: f6432d642ac5111cd4fe0cb6b041ada60af39ba41033195c949b0126184ae180
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d48777cee5009e48b5d3ef6c531029fdaa8c9629b37223ad919aacd53c29caf13258d7cddc28413788b61ec086af0abde606996cdac3c7012565ec2cea5b6e77
|
7
|
+
data.tar.gz: 8c9fa61a0f4b4c21b890c6895d4e74ba870fb92336cd30bb822e13bad5e49e9ee8d4b45e639171a73ab8c04e1756088e79979231a557a41ad9effa8ee1660e6b
|
data/Gemfile.lock
CHANGED
data/Guardfile
CHANGED
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
|
-
|
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
|
data/lib/quant/interval.rb
CHANGED
@@ -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
|
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
|
data/lib/quant/mixins/filters.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
|
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)
|
@@ -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
data/lib/quant/ticks/ohlc.rb
CHANGED
@@ -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
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
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.
|
data/lib/quant/time_methods.rb
CHANGED
@@ -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
|
data/lib/quant/time_period.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
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
|
-
|
70
|
-
other.lower_bound
|
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
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.
|
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-
|
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/
|
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
|