quantitative 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/Guardfile +1 -1
- data/Rakefile +6 -1
- data/lib/quant/attributes.rb +31 -43
- data/lib/quant/config.rb +8 -0
- data/lib/quant/experimental.rb +20 -0
- data/lib/quant/indicators/dominant_cycle_indicators.rb +10 -0
- data/lib/quant/indicators/dominant_cycles/acr.rb +101 -0
- data/lib/quant/indicators/dominant_cycles/band_pass.rb +80 -0
- data/lib/quant/indicators/dominant_cycles/differential.rb +19 -0
- data/lib/quant/indicators/dominant_cycles/dominant_cycle.rb +128 -0
- data/lib/quant/indicators/dominant_cycles/homodyne.rb +27 -0
- data/lib/quant/indicators/dominant_cycles/phase_accumulator.rb +59 -0
- data/lib/quant/indicators/indicator.rb +30 -8
- data/lib/quant/indicators/indicator_point.rb +12 -2
- data/lib/quant/indicators.rb +9 -2
- data/lib/quant/indicators_proxy.rb +0 -3
- data/lib/quant/indicators_sources.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_filters.rb +129 -0
- data/lib/quant/mixins/super_smoother.rb +18 -15
- data/lib/quant/mixins/universal_filters.rb +326 -0
- data/lib/quant/series.rb +1 -1
- data/lib/quant/statistics/correlation.rb +37 -0
- 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
- data/lib/quantitative.rb +1 -1
- metadata +13 -4
- data/lib/quant/indicators/ma.rb +0 -40
- data/lib/quant/mixins/high_pass_filter.rb +0 -54
@@ -0,0 +1,326 @@
|
|
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
|
+
# == Experimental!
|
20
|
+
# The main goal with the universal filters is to provide a means to
|
21
|
+
# compare the optimized filters with the generalized filters and
|
22
|
+
# generally show correctness of the solutions. However, that also
|
23
|
+
# means validating the outputs of those computations, which is not my forté.
|
24
|
+
# My idea of validating is if I have two or more implementations that produce
|
25
|
+
# identical (or nearly identical) results, then I consider the implementation
|
26
|
+
# sound and doing what it is supposed to do.
|
27
|
+
#
|
28
|
+
# Several are marked "experimental" because I have not been able to
|
29
|
+
# prove their correctness. Those that are proven correct are not
|
30
|
+
# marked as experimental and you'll find their outputs show up in other
|
31
|
+
# specs where they're used alongside the optimized versions of those filters.
|
32
|
+
#
|
33
|
+
# == Ehlers' Notes on Generalized Filters
|
34
|
+
# 1. All the common filters useful for traders have a transfer response
|
35
|
+
# that can be written as a ratio of two polynomials.
|
36
|
+
# 2. Lag is very important to traders. More complex filters can be
|
37
|
+
# created using more input data, but more input data increases lag.
|
38
|
+
# Sophisticated filters are not very useful for trading because they
|
39
|
+
# incur too much lag.
|
40
|
+
# 3. Filter transfer response can be viewed in the time domain and
|
41
|
+
# the frequency domain with equal validity.
|
42
|
+
# 4. Nonrecursive filters can have zeros in the transfer response, enabling
|
43
|
+
# the complete cancellation of some selected frequency components.
|
44
|
+
# 5. Nonrecursive filters having coefficients symmetrical about the
|
45
|
+
# center of the filter will have a delay of half the degree of the
|
46
|
+
# transfer response polynomial at all frequencies.
|
47
|
+
# 6. Low-pass filters are smoothers because they attenuate the high-frequency
|
48
|
+
# components of the input data.
|
49
|
+
# 7. High-pass filters are detrenders because they attenuate the
|
50
|
+
# low-frequency components of trends.
|
51
|
+
# 8. Band-pass filters are both detrenders and smoothers because they
|
52
|
+
# attenuate all but the desired frequency components.
|
53
|
+
# 9. Filters provide an output only through their transfer response.
|
54
|
+
# The transfer response is strictly a mathematical function, and
|
55
|
+
# interpretations such as overbought, oversold, convergence, divergence,
|
56
|
+
# and so on are not implied. The validity of such interpretations
|
57
|
+
# must be made on the basis of statistics apart from the filter.
|
58
|
+
# 10. The critical period of a filter output is the frequency at which
|
59
|
+
# the output power of the filter is half the power of the input
|
60
|
+
# wave at that frequency.
|
61
|
+
# 11. A WMA has little or no redeeming virtue.
|
62
|
+
# 12. A median filter is best used when the data contain impulsive noise
|
63
|
+
# or when there are wild variations in the data. Smoothing volume
|
64
|
+
# data is one example of a good application for a median filter.
|
65
|
+
#
|
66
|
+
# == Filter Coefficients forVariousTypes of Filters
|
67
|
+
#
|
68
|
+
# Filter Type b0 b1 b2 a1 a2
|
69
|
+
# EMA α 0 0 −(1−α) 0
|
70
|
+
# Two-pole low-pass α**2 0 0 −2*(1-α) (1-α)**2
|
71
|
+
# High-pass (1-α/2) -(1-α/2) 0 −(1−α) 0
|
72
|
+
# Two-pole high-pass (1-α/2)**2 -2*(1-α/2)**2 (1-α/2)**2 -2*(1-α) (1-α)**2
|
73
|
+
# Band-pass (1-σ)/2 0 -(1-σ)/2 -λ*(1+σ) σ
|
74
|
+
# Band-stop (1+σ)/2 -2λ*(1+σ)/2 (1+σ)/2 -λ*(1+σ) σ
|
75
|
+
#
|
76
|
+
module UniversalFilters
|
77
|
+
K = {
|
78
|
+
single_pole: 1.0,
|
79
|
+
two_pole_high_pass: Math.sqrt(2) * 0.5,
|
80
|
+
two_pole_low_pass: Math.sqrt(2)
|
81
|
+
}.freeze
|
82
|
+
|
83
|
+
# The universal filter is a generalization of the common filters. The other
|
84
|
+
# algorithms in this module are derived from this one.
|
85
|
+
def universal_filter(source, previous:, b0:, b1:, b2:, a1:, a2:)
|
86
|
+
b0 * p0.send(source) +
|
87
|
+
b1 * p1.send(source) +
|
88
|
+
b2 * p2.send(source) -
|
89
|
+
a1 * p1.send(previous) -
|
90
|
+
a2 * p2.send(previous)
|
91
|
+
end
|
92
|
+
|
93
|
+
# The EMA is optimized in the {Quant::Mixins::ExponentialMovingAverage} module
|
94
|
+
# and its correctness is proven with this particular implementation.
|
95
|
+
def universal_ema(source, previous:, period:)
|
96
|
+
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
97
|
+
raise ArgumentError, ":previous must be a Symbol" unless previous.is_a?(Symbol)
|
98
|
+
|
99
|
+
alpha = bars_to_alpha(period)
|
100
|
+
b0 = alpha
|
101
|
+
b1 = 0
|
102
|
+
b2 = 0
|
103
|
+
a1 = -(1 - alpha)
|
104
|
+
a2 = 0
|
105
|
+
|
106
|
+
universal_filter(source, previous:, b0:, b1:, b2:, a1:, a2:)
|
107
|
+
end
|
108
|
+
|
109
|
+
# The two-pole low-pass filter can serve several purposes:
|
110
|
+
#
|
111
|
+
# 1. Noise Reduction: Stock price data often contains high-frequency
|
112
|
+
# fluctuations or noise due to market volatility, algorithmic
|
113
|
+
# trading, or other factors. By applying a low-pass filter, you can
|
114
|
+
# smooth out these fluctuations, making it easier to identify
|
115
|
+
# underlying trends or patterns in the price action.
|
116
|
+
# 2. Trend Identification: Low-pass filtering can help in identifying
|
117
|
+
# the underlying trend in stock price movements by removing short-term
|
118
|
+
# fluctuations and emphasizing longer-term movements. This can be
|
119
|
+
# useful for trend-following strategies or identifying potential
|
120
|
+
# trend reversals.
|
121
|
+
# 3. Signal Smoothing: Filtering can help in removing erratic or
|
122
|
+
# spurious movements in the price data, providing a clearer and
|
123
|
+
# more consistent representation of the overall price action.
|
124
|
+
# 4. Highlighting Structural Changes: Filtering can make structural
|
125
|
+
# changes in the price action more apparent by reducing noise and
|
126
|
+
# focusing on significant movements. This can be useful for detecting
|
127
|
+
# shifts in market sentiment or the emergence of new trends.
|
128
|
+
# 5. Trade Signal Generation: Smoothed price data from a low-pass filter
|
129
|
+
# can be used as input for trading strategies, such as moving
|
130
|
+
# average-based strategies or momentum strategies, where identifying
|
131
|
+
# trends or momentum is crucial.
|
132
|
+
def universal_two_pole_low_pass(source, previous:, period:)
|
133
|
+
Quant.experimental("This method is unproven and may be incorrect.")
|
134
|
+
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
135
|
+
raise ArgumentError, ":previous must be a Symbol" unless previous.is_a?(Symbol)
|
136
|
+
|
137
|
+
alpha = period_to_alpha(period, k: K[:two_pole_low_pass] )
|
138
|
+
b0 = alpha**2
|
139
|
+
b1 = 0
|
140
|
+
b2 = 0
|
141
|
+
a1 = -2.0 * (1.0 - alpha)
|
142
|
+
a2 = (1.0 - alpha)**2
|
143
|
+
|
144
|
+
universal_filter(source, previous:, b0:, b1:, b2:, a1:, a2:)
|
145
|
+
end
|
146
|
+
|
147
|
+
# A single-pole low-pass filter, also known as a first-order low-pass
|
148
|
+
# filter, has a simpler response compared to a two-pole low-pass filter.
|
149
|
+
# It attenuates higher-frequency components of the signal while allowing
|
150
|
+
# lower-frequency components to pass through, but it does so with a
|
151
|
+
# gentler roll-off compared to higher-order filters.
|
152
|
+
#
|
153
|
+
# Here's what a single-pole low-pass filter typically does to stock
|
154
|
+
# price action data:
|
155
|
+
# 1. Noise Reduction: Similar to a two-pole low-pass filter, a single-pole
|
156
|
+
# filter can help in reducing high-frequency noise in stock price data,
|
157
|
+
# smoothing out rapid fluctuations caused by market volatility or other factors.
|
158
|
+
# 2. Trend Identification: It can aid in identifying trends by smoothing
|
159
|
+
# out short-term fluctuations, making the underlying trend more apparent.
|
160
|
+
# However, compared to higher-order filters, it may not provide as
|
161
|
+
# sharp or accurate trend signals.
|
162
|
+
# 3. Signal Smoothing: Single-pole filters provide a basic level of signal
|
163
|
+
# smoothing, which can help in removing minor fluctuations and emphasizing
|
164
|
+
# larger movements in the price action. This can make the data easier to
|
165
|
+
# interpret and analyze.
|
166
|
+
# 4. Delay and Responsiveness: Single-pole filters introduce less delay
|
167
|
+
# compared to higher-order filters, making them more responsive to changes
|
168
|
+
# in the input signal. However, this responsiveness comes at the cost of a
|
169
|
+
# less aggressive attenuation of high-frequency noise.
|
170
|
+
# 5. Simple Filtering: Single-pole filters are computationally efficient and
|
171
|
+
# easy to implement, making them suitable for real-time processing and
|
172
|
+
# applications where simplicity is preferred.
|
173
|
+
#
|
174
|
+
# Overall, while a single-pole low-pass filter can still be effective for
|
175
|
+
# noise reduction and basic trend identification in stock price action data,
|
176
|
+
# it may not offer the same level of precision and robustness as higher-order
|
177
|
+
# filters. The choice between single-pole and higher-order filters depends on
|
178
|
+
# the specific requirements of the analysis and the trade-offs between
|
179
|
+
# responsiveness and noise attenuation.
|
180
|
+
def universal_one_pole_low_pass(source, previous:, period:)
|
181
|
+
Quant.experimental("This method is unproven and may be incorrect.")
|
182
|
+
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
183
|
+
raise ArgumentError, ":previous must be a Symbol" unless previous.is_a?(Symbol)
|
184
|
+
|
185
|
+
alpha = period_to_alpha(period, k: K[:single_pole])
|
186
|
+
b0 = alpha
|
187
|
+
b1 = 0
|
188
|
+
b2 = 0
|
189
|
+
a1 = -(1 - alpha)
|
190
|
+
a2 = 0
|
191
|
+
|
192
|
+
universal_filter(source, previous:, b0:, b1:, b2:, a1:, a2:)
|
193
|
+
end
|
194
|
+
|
195
|
+
# A single-pole high-pass filter, also known as a first-order high-pass
|
196
|
+
# filter, attenuates low-frequency components of a signal while allowing
|
197
|
+
# higher-frequency components to pass through. In the context of processing
|
198
|
+
# stock price action data, applying a single-pole high-pass filter has
|
199
|
+
# several potential effects:
|
200
|
+
#
|
201
|
+
# 1. Removal of Low-Frequency Trends: Similar to higher-order high-pass
|
202
|
+
# filters, a single-pole high-pass filter removes or attenuates slow-moving
|
203
|
+
# components of the price action data, such as long-term trends. This can
|
204
|
+
# help in focusing on shorter-term fluctuations and identifying short-term
|
205
|
+
# trading opportunities.
|
206
|
+
# 2. Noise Amplification: As with higher-order high-pass filters, a single-pole
|
207
|
+
# high-pass filter can amplify high-frequency noise present in the data,
|
208
|
+
# especially if the cutoff frequency is set too low. This noise amplification
|
209
|
+
# can make it challenging to analyze the data accurately, particularly if the
|
210
|
+
# noise overwhelms the signal of interest.
|
211
|
+
# 3. Emphasis on Short-Term Variations: By removing low-frequency components, a
|
212
|
+
# single-pole high-pass filter highlights short-term variations and rapid
|
213
|
+
# movements in the price action data. This can be beneficial for traders or
|
214
|
+
# analysts who are primarily interested in short-term price dynamics or
|
215
|
+
# intraday trading opportunities.
|
216
|
+
# 4. Simple Filtering: Single-pole filters are computationally efficient and
|
217
|
+
# straightforward to implement, making them suitable for real-time processing
|
218
|
+
# and applications where simplicity is preferred. However, they may not offer
|
219
|
+
# the same level of noise attenuation and signal preservation as higher-order
|
220
|
+
# filters.
|
221
|
+
# 5. Enhanced Responsiveness: Single-pole high-pass filters offer relatively high
|
222
|
+
# responsiveness to changes in the input signal, reflecting recent price movements
|
223
|
+
# quickly. This responsiveness can be advantageous for certain trading strategies
|
224
|
+
# that rely on timely identification of market events or short-term trends.
|
225
|
+
#
|
226
|
+
# Overall, applying a single-pole high-pass filter to stock price action data can
|
227
|
+
# help in removing low-frequency trends and focusing on short-term variations and
|
228
|
+
# rapid movements. However, it's essential to carefully select the cutoff frequency
|
229
|
+
# to balance noise attenuation with signal preservation and to consider the potential
|
230
|
+
# trade-offs between simplicity and filtering effectiveness.
|
231
|
+
def universal_one_pole_high_pass(source, previous:, period:)
|
232
|
+
Quant.experimental("This method is unproven and may be incorrect.")
|
233
|
+
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
234
|
+
raise ArgumentError, ":previous must be a Symbol" unless previous.is_a?(Symbol)
|
235
|
+
|
236
|
+
alpha = period_to_alpha(period, k: K[:single_pole])
|
237
|
+
b0 = (1 - alpha / 2)
|
238
|
+
b1 = -(1 - alpha / 2)
|
239
|
+
b2 = 0
|
240
|
+
a1 = -(1 - alpha)
|
241
|
+
a2 = 0
|
242
|
+
|
243
|
+
universal_filter(source, previous:, b0:, b1:, b2:, a1:, a2:)
|
244
|
+
end
|
245
|
+
|
246
|
+
# A two-pole high-pass filter, also known as a second-order high-pass
|
247
|
+
# filter, attenuates low-frequency components of a signal while allowing
|
248
|
+
# higher-frequency components to pass through. In the context of
|
249
|
+
# processing stock price action data, applying a two-pole high-pass
|
250
|
+
# filter has several potential effects:
|
251
|
+
#
|
252
|
+
# 1. Removal of Low-Frequency Trends: High-pass filtering can remove
|
253
|
+
# or attenuate long-term trends or slow-moving components of the
|
254
|
+
# price action data. This can be useful for focusing on shorter-term
|
255
|
+
# fluctuations or identifying short-term trading opportunities.
|
256
|
+
# 2. Noise Attenuation: By suppressing low-frequency components, a
|
257
|
+
# high-pass filter can help in reducing the impact of slow-moving
|
258
|
+
# noise or irrelevant signals in the data. This can improve the
|
259
|
+
# clarity and interpretability of the price action data.
|
260
|
+
# 3. Noise Amplification: High-pass filters can amplify high-frequency
|
261
|
+
# noise present in the data, particularly if the cutoff frequency is
|
262
|
+
# set too low. This noise amplification can make it challenging to
|
263
|
+
# analyze the data accurately, especially if the noise overwhelms
|
264
|
+
# the signal of interest.
|
265
|
+
# 4. Emphasis on Short-Term Variations: By removing low-frequency
|
266
|
+
# components, a high-pass filter highlights short-term variations
|
267
|
+
# and rapid movements in the price action data. This can be beneficial
|
268
|
+
# for traders or analysts who are primarily interested in short-term
|
269
|
+
# price dynamics.
|
270
|
+
# 5. Enhanced Responsiveness: Compared to low-pass filters, high-pass
|
271
|
+
# filters typically offer greater responsiveness to changes in the
|
272
|
+
# input signal. This means that high-pass filtered data can reflect
|
273
|
+
# recent price movements more quickly, which may be advantageous for
|
274
|
+
# certain trading strategies.
|
275
|
+
# 6. Identification of Market Events: High-pass filtering can help in
|
276
|
+
# identifying specific market events or anomalies that occur on
|
277
|
+
# shorter time scales, such as intraday price spikes or volatility
|
278
|
+
# clusters.
|
279
|
+
#
|
280
|
+
# Overall, applying a two-pole high-pass filter to stock price action
|
281
|
+
# data can help in focusing on short-term variations and removing
|
282
|
+
# long-term trends or slow-moving components. However, it's essential
|
283
|
+
# to carefully select the cutoff frequency to balance noise attenuation
|
284
|
+
# with signal preservation, as excessive noise amplification can degrade
|
285
|
+
# the quality of the analysis. Additionally, high-pass filtering may
|
286
|
+
# not be suitable for all trading or analysis purposes, and its effects
|
287
|
+
# should be evaluated in the context of specific goals and strategies.
|
288
|
+
def universal_two_pole_high_pass(source, previous:, period:)
|
289
|
+
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
290
|
+
raise ArgumentError, ":previous must be a Symbol" unless previous.is_a?(Symbol)
|
291
|
+
|
292
|
+
alpha = period_to_alpha(period, k: K[:two_pole_high_pass])
|
293
|
+
b0 = (1 - alpha / 2)**2
|
294
|
+
b1 = -2 * (1 - alpha / 2)**2
|
295
|
+
b2 = (1 - alpha / 2)**2
|
296
|
+
a1 = -2 * (1 - alpha)
|
297
|
+
a2 = (1 - alpha)**2
|
298
|
+
|
299
|
+
universal_filter(source, previous:, b0:, b1:, b2:, a1:, a2:)
|
300
|
+
end
|
301
|
+
|
302
|
+
# Band-pass filters are both detrenders and smoothers because they
|
303
|
+
# attenuate all but the desired frequency components.
|
304
|
+
# NOTE: Ehlers' book contains a typo in the formula for the band-pass
|
305
|
+
# filter. I am not sure what the correct formulation is, so
|
306
|
+
# this is a best guess for how, left for further investigation.
|
307
|
+
def universal_band_pass(source, previous:, period:)
|
308
|
+
Quant.experimental("This method is unproven and may be incorrect.")
|
309
|
+
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
310
|
+
raise ArgumentError, ":previous must be a Symbol" unless previous.is_a?(Symbol)
|
311
|
+
|
312
|
+
lambda = deg2rad(360.0 / period)
|
313
|
+
gamma = Math.cos(lambda)
|
314
|
+
sigma = 1 / gamma - Math.sqrt(1 / gamma**2 - 1)
|
315
|
+
|
316
|
+
b0 = (1 - sigma) * 0.5
|
317
|
+
b1 = 0
|
318
|
+
b2 = -(1 - sigma) * 0.5
|
319
|
+
a1 = -lambda * (1 + sigma)
|
320
|
+
a2 = sigma
|
321
|
+
|
322
|
+
universal_filter(source, previous:, b0:, b1:, b2:, a1:, a2:)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
data/lib/quant/series.rb
CHANGED
@@ -0,0 +1,37 @@
|
|
1
|
+
module Quant
|
2
|
+
module Statistics
|
3
|
+
class Correlation
|
4
|
+
attr_accessor :length, :sx, :sy, :sxx, :sxy, :syy
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@length = 0.0
|
8
|
+
@sx = 0.0
|
9
|
+
@sy = 0.0
|
10
|
+
@sxx = 0.0
|
11
|
+
@sxy = 0.0
|
12
|
+
@syy = 0.0
|
13
|
+
end
|
14
|
+
|
15
|
+
def add(x, y)
|
16
|
+
@length += 1
|
17
|
+
@sx += x
|
18
|
+
@sy += y
|
19
|
+
@sxx += x * x
|
20
|
+
@sxy += x * y
|
21
|
+
@syy += y * y
|
22
|
+
end
|
23
|
+
|
24
|
+
def devisor
|
25
|
+
value = (length * sxx - sx**2) * (length * syy - sy**2)
|
26
|
+
value.zero? ? 1.0 : value
|
27
|
+
end
|
28
|
+
|
29
|
+
def coefficient
|
30
|
+
(length * sxy - sx * sy) / Math.sqrt(devisor)
|
31
|
+
rescue Math::DomainError
|
32
|
+
0.0
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
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
data/lib/quantitative.rb
CHANGED
@@ -12,6 +12,6 @@ quant_folder = File.join(lib_folder, "quant")
|
|
12
12
|
Dir.glob(File.join(quant_folder, "*.rb")).each { |fn| require fn }
|
13
13
|
|
14
14
|
# require sub-folders and their sub-folders
|
15
|
-
%w(refinements mixins settings ticks indicators).each do |sub_folder|
|
15
|
+
%w(refinements mixins statistics settings ticks indicators).each do |sub_folder|
|
16
16
|
Dir.glob(File.join(quant_folder, sub_folder, "**/*.rb")).each { |fn| require fn }
|
17
17
|
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.
|
4
|
+
version: 0.2.0
|
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-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|
@@ -49,10 +49,17 @@ 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
|
54
|
+
- lib/quant/indicators/dominant_cycle_indicators.rb
|
55
|
+
- lib/quant/indicators/dominant_cycles/acr.rb
|
56
|
+
- lib/quant/indicators/dominant_cycles/band_pass.rb
|
57
|
+
- lib/quant/indicators/dominant_cycles/differential.rb
|
58
|
+
- lib/quant/indicators/dominant_cycles/dominant_cycle.rb
|
59
|
+
- lib/quant/indicators/dominant_cycles/homodyne.rb
|
60
|
+
- lib/quant/indicators/dominant_cycles/phase_accumulator.rb
|
53
61
|
- lib/quant/indicators/indicator.rb
|
54
62
|
- lib/quant/indicators/indicator_point.rb
|
55
|
-
- lib/quant/indicators/ma.rb
|
56
63
|
- lib/quant/indicators/ping.rb
|
57
64
|
- lib/quant/indicators_proxy.rb
|
58
65
|
- lib/quant/indicators_sources.rb
|
@@ -63,17 +70,19 @@ files:
|
|
63
70
|
- lib/quant/mixins/filters.rb
|
64
71
|
- lib/quant/mixins/fisher_transform.rb
|
65
72
|
- lib/quant/mixins/functions.rb
|
66
|
-
- lib/quant/mixins/
|
73
|
+
- lib/quant/mixins/high_pass_filters.rb
|
67
74
|
- lib/quant/mixins/hilbert_transform.rb
|
68
75
|
- lib/quant/mixins/moving_averages.rb
|
69
76
|
- lib/quant/mixins/simple_moving_average.rb
|
70
77
|
- lib/quant/mixins/stochastic.rb
|
71
78
|
- lib/quant/mixins/super_smoother.rb
|
79
|
+
- lib/quant/mixins/universal_filters.rb
|
72
80
|
- lib/quant/mixins/weighted_moving_average.rb
|
73
81
|
- lib/quant/refinements/array.rb
|
74
82
|
- lib/quant/series.rb
|
75
83
|
- lib/quant/settings.rb
|
76
84
|
- lib/quant/settings/indicators.rb
|
85
|
+
- lib/quant/statistics/correlation.rb
|
77
86
|
- lib/quant/ticks/ohlc.rb
|
78
87
|
- lib/quant/ticks/serializers/ohlc.rb
|
79
88
|
- lib/quant/ticks/serializers/spot.rb
|
data/lib/quant/indicators/ma.rb
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Quant
|
4
|
-
class Indicators
|
5
|
-
class MaPoint < IndicatorPoint
|
6
|
-
attribute :ss, key: "ss"
|
7
|
-
attribute :ema, key: "ema"
|
8
|
-
attr_accessor :ss, :ema, :osc
|
9
|
-
|
10
|
-
def initialize_data_points
|
11
|
-
@ss = input
|
12
|
-
@ema = input
|
13
|
-
@osc = nil
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
# Moving Averages
|
18
|
-
class Ma < Indicator
|
19
|
-
include Quant::Mixins::Filters
|
20
|
-
|
21
|
-
def alpha(period)
|
22
|
-
bars_to_alpha(period)
|
23
|
-
end
|
24
|
-
|
25
|
-
def min_period
|
26
|
-
8 # Quant.config.indicators.min_period
|
27
|
-
end
|
28
|
-
|
29
|
-
def max_period
|
30
|
-
48 # Quant.config.indicators.max_period
|
31
|
-
end
|
32
|
-
|
33
|
-
def compute
|
34
|
-
# p0.ss = super_smoother input, :ss, min_period
|
35
|
-
p0.ema = alpha(max_period) * input + (1 - alpha(max_period)) * p1.ema
|
36
|
-
p0.osc = p0.ss - p0.ema
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
@@ -1,54 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Quant
|
4
|
-
module Mixins
|
5
|
-
module HighPassFilter
|
6
|
-
# HighPass Filters are “detrenders” because they attenuate low frequency components
|
7
|
-
# One pole HighPass and SuperSmoother does not produce a zero mean because low
|
8
|
-
# frequency spectral dilation components are “leaking” through The one pole
|
9
|
-
# HighPass Filter response
|
10
|
-
def two_pole_high_pass_filter(source, prev_source, min_period, max_period = nil)
|
11
|
-
raise "source must be a symbol" unless source.is_a?(Symbol)
|
12
|
-
return p0.send(source) if p0 == p2
|
13
|
-
|
14
|
-
max_period ||= min_period * 2
|
15
|
-
(min_period * Math.sqrt(2))
|
16
|
-
max_radians = 2.0 * Math::PI / (max_period * Math.sqrt(2))
|
17
|
-
|
18
|
-
v1 = p0.send(source) - (2.0 * p1.send(source)) + p2.send(source)
|
19
|
-
v2 = p1.send(prev_source)
|
20
|
-
v3 = p2.send(prev_source)
|
21
|
-
|
22
|
-
alpha = period_to_alpha(max_radians)
|
23
|
-
|
24
|
-
a = (1 - (alpha * 0.5))**2 * v1
|
25
|
-
b = 2 * (1 - alpha) * v2
|
26
|
-
c = (1 - alpha)**2 * v3
|
27
|
-
|
28
|
-
a + b - c
|
29
|
-
end
|
30
|
-
|
31
|
-
# alpha = (Cosine(.707* 2 * PI / 48) + Sine (.707*360 / 48) - 1) / Cosine(.707*360 / 48);
|
32
|
-
# is the same as the following:
|
33
|
-
# radians = Math.sqrt(2) * Math::PI / period
|
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
|
37
|
-
return v0 if p3 == p0
|
38
|
-
|
39
|
-
v1 = p1.send(source)
|
40
|
-
v2 = p2.send(source)
|
41
|
-
|
42
|
-
radians = Math.sqrt(2) * Math::PI / period
|
43
|
-
a = Math.exp(-radians)
|
44
|
-
b = 2 * a * Math.cos(radians)
|
45
|
-
|
46
|
-
c2 = b
|
47
|
-
c3 = -a**2
|
48
|
-
c1 = (1 + c2 - c3) / 4
|
49
|
-
|
50
|
-
(c1 * (v0 - (2 * v1) + v2)) + (c2 * p1.hp) + (c3 * p2.hp)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|