quantitative 0.2.0 → 0.2.2
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 +9 -4
- data/Gemfile.lock +9 -1
- data/lib/quant/dominant_cycle_indicators.rb +52 -0
- data/lib/quant/errors.rb +4 -0
- data/lib/quant/indicators/adx.rb +80 -0
- data/lib/quant/indicators/atr.rb +79 -0
- data/lib/quant/indicators/cci.rb +63 -0
- data/lib/quant/indicators/decycler.rb +85 -0
- data/lib/quant/indicators/dominant_cycles/acr.rb +14 -12
- data/lib/quant/indicators/dominant_cycles/band_pass.rb +7 -0
- data/lib/quant/indicators/dominant_cycles/differential.rb +8 -4
- data/lib/quant/indicators/dominant_cycles/dominant_cycle.rb +19 -6
- data/lib/quant/indicators/dominant_cycles/half_period.rb +23 -0
- data/lib/quant/indicators/dominant_cycles/homodyne.rb +7 -4
- data/lib/quant/indicators/dominant_cycles/phase_accumulator.rb +9 -5
- data/lib/quant/indicators/frama.rb +53 -0
- data/lib/quant/indicators/indicator.rb +14 -0
- data/lib/quant/indicators/mama.rb +143 -0
- data/lib/quant/indicators/mesa.rb +86 -0
- data/lib/quant/indicators.rb +5 -1
- data/lib/quant/indicators_proxy.rb +11 -1
- data/lib/quant/indicators_sources.rb +10 -0
- data/lib/quant/mixins/stochastic.rb +1 -1
- data/lib/quant/mixins/super_smoother.rb +11 -2
- data/lib/quant/series.rb +14 -0
- data/lib/quant/settings/indicators.rb +16 -6
- data/lib/quant/settings.rb +1 -1
- data/lib/quant/version.rb +1 -1
- metadata +11 -3
- data/lib/quant/indicators/dominant_cycle_indicators.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6b5786b75635cde82e7919fce6b8fcd568724b27fc2810efe6db36a7e6dd09cc
|
4
|
+
data.tar.gz: 3a6fb86c25bb4f2b1e4994710539bfb528377ef57531e6845a4417c87fc86e29
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ae30c5b971d6c7bd41e3c2b764e7d95438931790271133dc0e3a476a371affddfdcf06355ffa933240c0c9952fd8fa5e4212c496c95c80f028de7399d3b129f
|
7
|
+
data.tar.gz: 9ff8aeaf24800a7208e51f836dd7c7c3621b2e2e3a7f1e8820d89f6fc5cd56dd3c4b762c478d221c6ef7279f62c0df75ae1a19f6a4d34e7ba82e116e6a10a790
|
data/Gemfile
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
# Specify your gem
|
5
|
+
# Specify your gem"s dependencies in quantitative.gemspec
|
6
6
|
gemspec
|
7
7
|
|
8
8
|
gem "rake", "~> 13.0"
|
@@ -18,7 +18,12 @@ gem "guard-rspec", "~> 4.7"
|
|
18
18
|
gem "yard", "~> 0.9"
|
19
19
|
gem "benchmark-ips", "~> 2.9"
|
20
20
|
|
21
|
-
gem
|
22
|
-
gem 'simplecov'
|
23
|
-
gem 'simplecov-cobertura'
|
21
|
+
gem "rspec-github"
|
24
22
|
|
23
|
+
# Test coverage and profiling
|
24
|
+
gem "simplecov"
|
25
|
+
gem "simplecov-cobertura"
|
26
|
+
gem "stackprof", require: false
|
27
|
+
gem "test-prof", require: false
|
28
|
+
gem "vernier", require: false
|
29
|
+
gem "ruby-prof", require: false
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
quantitative (0.2.
|
4
|
+
quantitative (0.2.2)
|
5
5
|
oj (~> 3.10)
|
6
6
|
|
7
7
|
GEM
|
@@ -107,6 +107,7 @@ GEM
|
|
107
107
|
rubocop (~> 1.40)
|
108
108
|
rubocop-capybara (~> 2.17)
|
109
109
|
rubocop-factory_bot (~> 2.22)
|
110
|
+
ruby-prof (1.7.0)
|
110
111
|
ruby-progressbar (1.13.0)
|
111
112
|
shellany (0.0.1)
|
112
113
|
simplecov (0.22.0)
|
@@ -118,9 +119,12 @@ GEM
|
|
118
119
|
simplecov (~> 0.19)
|
119
120
|
simplecov-html (0.12.3)
|
120
121
|
simplecov_json_formatter (0.1.4)
|
122
|
+
stackprof (0.2.26)
|
121
123
|
stringio (3.1.0)
|
124
|
+
test-prof (1.3.2)
|
122
125
|
thor (1.3.0)
|
123
126
|
unicode-display_width (2.5.0)
|
127
|
+
vernier (0.5.1)
|
124
128
|
yard (0.9.34)
|
125
129
|
|
126
130
|
PLATFORMS
|
@@ -138,8 +142,12 @@ DEPENDENCIES
|
|
138
142
|
rspec-github
|
139
143
|
rubocop (~> 1.21)
|
140
144
|
rubocop-rspec
|
145
|
+
ruby-prof
|
141
146
|
simplecov
|
142
147
|
simplecov-cobertura
|
148
|
+
stackprof
|
149
|
+
test-prof
|
150
|
+
vernier
|
143
151
|
yard (~> 0.9)
|
144
152
|
|
145
153
|
BUNDLED WITH
|
@@ -0,0 +1,52 @@
|
|
1
|
+
|
2
|
+
require_relative 'indicators_proxy'
|
3
|
+
|
4
|
+
module Quant
|
5
|
+
# Dominant Cycles measure the primary cycle within a given range. By default, the library
|
6
|
+
# is wired to look for cycles between 10 and 48 bars. These values can be adjusted by setting
|
7
|
+
# the `min_period` and `max_period` configuration values in {Quant::Config}.
|
8
|
+
#
|
9
|
+
# Quant.configure_indicators(min_period: 8, max_period: 32)
|
10
|
+
#
|
11
|
+
# The default dominant cycle kind is the `half_period` filter. This can be adjusted by setting
|
12
|
+
# the `dominant_cycle_kind` configuration value in {Quant::Config}.
|
13
|
+
#
|
14
|
+
# Quant.configure_indicators(dominant_cycle_kind: :band_pass)
|
15
|
+
#
|
16
|
+
# The purpose of these indicators is to compute the dominant cycle and underpin the various
|
17
|
+
# indicators that would otherwise be setting an arbitrary lookback period. This makes the
|
18
|
+
# indicators adaptive and auto-tuning to the market dynamics. Or so the theory goes!
|
19
|
+
class DominantCycleIndicators < IndicatorsProxy
|
20
|
+
# Auto-Correlation Reversals is a method of computing the dominant cycle
|
21
|
+
# by correlating the data stream with itself delayed by a lag.
|
22
|
+
def acr; indicator(Indicators::DominantCycles::Acr) end
|
23
|
+
|
24
|
+
# The band-pass dominant cycle passes signals within a certain frequency
|
25
|
+
# range, and attenuates signals outside that range.
|
26
|
+
# The trend component of the signal is removed, leaving only the cyclical
|
27
|
+
# component. Then we count number of iterations between zero crossings
|
28
|
+
# and this is the `period` of the dominant cycle.
|
29
|
+
def band_pass; indicator(Indicators::DominantCycles::BandPass) end
|
30
|
+
|
31
|
+
# Homodyne means the signal is multiplied by itself. More precisely,
|
32
|
+
# we want to multiply the signal of the current bar with the complex
|
33
|
+
# value of the signal one bar ago
|
34
|
+
def homodyne; indicator(Indicators::DominantCycles::Homodyne) end
|
35
|
+
|
36
|
+
# The Dual Differentiator algorithm computes the phase angle from the
|
37
|
+
# analytic signal as the arctangent of the ratio of the imaginary
|
38
|
+
# component to the real component. Further, the angular frequency
|
39
|
+
# is defined as the rate change of phase. We can use these facts to
|
40
|
+
# derive the cycle period.
|
41
|
+
def differential; indicator(Indicators::DominantCycles::Differential) end
|
42
|
+
|
43
|
+
# The phase accumulation method of computing the dominant cycle measures
|
44
|
+
# the phase at each sample by taking the arctangent of the ratio of the
|
45
|
+
# quadrature component to the in-phase component. The phase is then
|
46
|
+
# accumulated and the period is derived from the phase.
|
47
|
+
def phase_accumulator; indicator(Indicators::DominantCycles::PhaseAccumulator) end
|
48
|
+
|
49
|
+
# Static, arbitrarily set period.
|
50
|
+
def half_period; indicator(Indicators::DominantCycles::HalfPeriod) end
|
51
|
+
end
|
52
|
+
end
|
data/lib/quant/errors.rb
CHANGED
@@ -10,6 +10,10 @@ module Quant
|
|
10
10
|
# {Quant::Interval} with an invalid value.
|
11
11
|
class InvalidInterval < Error; end
|
12
12
|
|
13
|
+
# {InvalidIndicatorSource} is raised when attempting to reference
|
14
|
+
# an indicator through a source that has not been prepared, yet.
|
15
|
+
class InvalidIndicatorSource < Error; end
|
16
|
+
|
13
17
|
# {InvalidResolution} is raised when attempting to instantiate
|
14
18
|
# an {Quant::Resolution} with a resolution value that has not been defined.
|
15
19
|
class InvalidResolution < Error; end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "indicator_point"
|
4
|
+
require_relative "indicator"
|
5
|
+
|
6
|
+
module Quant
|
7
|
+
class Indicators
|
8
|
+
class AdxPoint < IndicatorPoint
|
9
|
+
attribute :dmu, default: 0.0
|
10
|
+
attribute :dmd, default: 0.0
|
11
|
+
attribute :dmu_ema, default: 0.0
|
12
|
+
attribute :dmd_ema, default: 0.0
|
13
|
+
attribute :diu, default: 0.0
|
14
|
+
attribute :did, default: 0.0
|
15
|
+
attribute :di, default: 0.0
|
16
|
+
attribute :di_ema, default: 0.0
|
17
|
+
attribute :value, default: 0.0
|
18
|
+
attribute :inst_stoch, default: 0.0
|
19
|
+
attribute :stoch, default: 0.0
|
20
|
+
attribute :stoch_up, default: false
|
21
|
+
attribute :stoch_turned, default: false
|
22
|
+
attribute :ssf, default: 0.0
|
23
|
+
attribute :hp, default: 0.0
|
24
|
+
end
|
25
|
+
|
26
|
+
class Adx < Indicator
|
27
|
+
def alpha
|
28
|
+
bars_to_alpha(dc_period)
|
29
|
+
end
|
30
|
+
|
31
|
+
def scale
|
32
|
+
1.0
|
33
|
+
end
|
34
|
+
|
35
|
+
def period
|
36
|
+
dc_period
|
37
|
+
end
|
38
|
+
|
39
|
+
def atr_point
|
40
|
+
series.indicators[source].atr.points[t0]
|
41
|
+
end
|
42
|
+
|
43
|
+
def compute
|
44
|
+
# To calculate the ADX, first determine the + and - directional movement, or DM.
|
45
|
+
# The +DM and -DM are found by calculating the "up-move," or current high minus
|
46
|
+
# the previous high, and "down-move," or current low minus the previous low.
|
47
|
+
# If the up-move is greater than the down-move and greater than zero, the +DM equals the up-move;
|
48
|
+
# otherwise, it equals zero. If the down-move is greater than the up-move and greater than zero,
|
49
|
+
# the -DM equals the down-move; otherwise, it equals zero.
|
50
|
+
dm_highs = [t0.high_price - t1.high_price, 0.0].max
|
51
|
+
dm_lows = [t0.low_price - t1.low_price, 0.0].max
|
52
|
+
|
53
|
+
p0.dmu = dm_highs > dm_lows ? 0.0 : dm_highs
|
54
|
+
p0.dmd = dm_lows > dm_highs ? 0.0 : dm_lows
|
55
|
+
|
56
|
+
p0.dmu_ema = three_pole_super_smooth :dmu, period:, previous: :dmu_ema
|
57
|
+
p0.dmd_ema = three_pole_super_smooth :dmd, period:, previous: :dmd_ema
|
58
|
+
|
59
|
+
atr_value = atr_point.fast * scale
|
60
|
+
return if atr_value == 0.0 || @points.size < period
|
61
|
+
|
62
|
+
# The positive directional indicator, or +DI, equals 100 times the EMA of +DM divided by the ATR
|
63
|
+
# over a given number of time periods. Welles usually used 14 periods.
|
64
|
+
# The negative directional indicator, or -DI, equals 100 times the EMA of -DM divided by the ATR.
|
65
|
+
p0.diu = (100.0 * p0.dmu_ema) / atr_value
|
66
|
+
p0.did = (100.0 * p0.dmd_ema) / atr_value
|
67
|
+
|
68
|
+
# The ADX indicator itself equals 100 times the EMA of the absolute value of (+DI minus -DI)
|
69
|
+
# divided by (+DI plus -DI).
|
70
|
+
delta = p0.diu + p0.did
|
71
|
+
p0.di = (p0.diu - p1.did).abs / delta
|
72
|
+
p0.di_ema = three_pole_super_smooth(:di, period:, previous: :di_ema).clamp(-10.0, 10.0)
|
73
|
+
|
74
|
+
p0.value = p0.di_ema
|
75
|
+
p0.inst_stoch = stochastic :di, period: dc_period
|
76
|
+
p0.stoch = three_pole_super_smooth :inst_stoch, period:, previous: :stoch
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "indicator_point"
|
4
|
+
require_relative "indicator"
|
5
|
+
|
6
|
+
module Quant
|
7
|
+
class Indicators
|
8
|
+
class AtrPoint < IndicatorPoint
|
9
|
+
attribute :tr, default: 0.0
|
10
|
+
attribute :period, default: :min_period
|
11
|
+
attribute :value, default: 0.0
|
12
|
+
attribute :slow, default: 0.0
|
13
|
+
attribute :fast, default: 0.0
|
14
|
+
attribute :inst_stoch, default: 0.0
|
15
|
+
attribute :stoch, default: 0.0
|
16
|
+
attribute :stoch_up, default: false
|
17
|
+
attribute :stoch_turned, default: false
|
18
|
+
attribute :osc, default: 0.0
|
19
|
+
attribute :crossed, default: :unchanged
|
20
|
+
|
21
|
+
def crossed_up?
|
22
|
+
@crossed == :up
|
23
|
+
end
|
24
|
+
|
25
|
+
def crossed_down?
|
26
|
+
@crossed == :down
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Atr < Indicator
|
31
|
+
attr_reader :points
|
32
|
+
|
33
|
+
def period
|
34
|
+
dc_period / 2
|
35
|
+
end
|
36
|
+
|
37
|
+
def fast_alpha
|
38
|
+
period_to_alpha(period)
|
39
|
+
end
|
40
|
+
|
41
|
+
def slow_alpha
|
42
|
+
period_to_alpha(2 * period)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Typically, the Average True Range (ATR) is based on 14 periods and can be calculated on an intraday, daily, weekly
|
46
|
+
# or monthly basis. For this example, the ATR will be based on daily data. Because there must be a beginning, the first
|
47
|
+
# TR value is simply the High minus the Low, and the first 14-day ATR is the average of the daily TR values for the
|
48
|
+
# last 14 days. After that, Wilder sought to smooth the data by incorporating the previous period's ATR value.
|
49
|
+
|
50
|
+
# Current ATR = [(Prior ATR x 13) + Current TR] / 14
|
51
|
+
|
52
|
+
# - Multiply the previous 14-day ATR by 13.
|
53
|
+
# - Add the most recent day's TR value.
|
54
|
+
# - Divide the total by 14
|
55
|
+
|
56
|
+
def compute
|
57
|
+
p0.period = period
|
58
|
+
p0.tr = (t1.high_price - t0.close_price).abs
|
59
|
+
|
60
|
+
p0.value = three_pole_super_smooth :tr, period:, previous: :value
|
61
|
+
|
62
|
+
p0.slow = (slow_alpha * p0.value) + ((1.0 - slow_alpha) * p1.slow)
|
63
|
+
p0.fast = (fast_alpha * p0.value) + ((1.0 - fast_alpha) * p1.fast)
|
64
|
+
|
65
|
+
p0.inst_stoch = stochastic :value, period:
|
66
|
+
p0.stoch = three_pole_super_smooth(:inst_stoch, previous: :stoch, period:).clamp(0, 100)
|
67
|
+
p0.stoch_up = p0.stoch >= 70
|
68
|
+
p0.stoch_turned = p0.stoch_up && !p1.stoch_up
|
69
|
+
compute_oscillator
|
70
|
+
end
|
71
|
+
|
72
|
+
def compute_oscillator
|
73
|
+
p0.osc = p0.value - wma(:value)
|
74
|
+
p0.crossed = :up if p0.osc >= 0 && p1.osc < 0
|
75
|
+
p0.crossed = :down if p0.osc <= 0 && p1.osc > 0
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class Indicators
|
5
|
+
class CciPoint < IndicatorPoint
|
6
|
+
attribute :hp, default: 0.0
|
7
|
+
attribute :real, default: 0.0
|
8
|
+
attribute :imag, default: 0.0
|
9
|
+
attribute :angle, default: 0.0
|
10
|
+
attribute :state, default: 0
|
11
|
+
end
|
12
|
+
|
13
|
+
# Correlation Cycle Index
|
14
|
+
# The very definition of a trend mode and a cycle mode makes it simple
|
15
|
+
# to create a state variable that identifies the market state. If the
|
16
|
+
# state is zero, the market is in a cycle mode. If the state is +1 the
|
17
|
+
# market is in a trend up. If the state is -1 the market is in a trend down.
|
18
|
+
#
|
19
|
+
# SOURCE: https://www.mesasoftware.com/papers/CORRELATION%20AS%20A%20CYCLE%20INDICATOR.pdf
|
20
|
+
class Cci < Indicator
|
21
|
+
def max_period
|
22
|
+
[min_period, dc_period].max
|
23
|
+
end
|
24
|
+
|
25
|
+
def compute_correlations
|
26
|
+
corr_real = Statistics::Correlation.new
|
27
|
+
corr_imag = Statistics::Correlation.new
|
28
|
+
arc = 2.0 * Math::PI / max_period.to_f
|
29
|
+
(0...max_period).each do |period|
|
30
|
+
radians = arc * period
|
31
|
+
prev_hp = p(period).hp
|
32
|
+
corr_real.add(prev_hp, Math.cos(radians))
|
33
|
+
corr_imag.add(prev_hp, -Math.sin(radians))
|
34
|
+
end
|
35
|
+
p0.real = corr_real.coefficient
|
36
|
+
p0.imag = corr_imag.coefficient
|
37
|
+
end
|
38
|
+
|
39
|
+
def compute_angle
|
40
|
+
# Compute the angle as an arctangent and resolve the quadrant
|
41
|
+
p0.angle = 90 + rad2deg(Math.atan(p0.real / p0.imag))
|
42
|
+
p0.angle -= 180 if p0.imag > 0
|
43
|
+
|
44
|
+
# Do not allow the rate change of angle to go negative
|
45
|
+
p0.angle = p1.angle if (p0.angle < p1.angle) && (p1.angle - p0.angle) < 270
|
46
|
+
end
|
47
|
+
|
48
|
+
def compute_state
|
49
|
+
return unless (p0.angle - p1.angle).abs < 9
|
50
|
+
|
51
|
+
p0.state = p0.angle < 0 ? -1 : 1
|
52
|
+
end
|
53
|
+
|
54
|
+
def compute
|
55
|
+
p0.hp = two_pole_butterworth :input, previous: :hp, period: min_period
|
56
|
+
|
57
|
+
compute_correlations
|
58
|
+
compute_angle
|
59
|
+
compute_state
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class Indicators
|
5
|
+
# The decycler oscillator can be useful for determining the transition be- tween uptrends and downtrends by the crossing of the zero
|
6
|
+
# line. Alternatively, the changes of slope of the decycler oscillator are easier to identify than the changes in slope of the
|
7
|
+
# original decycler. Optimum cutoff periods can easily be found by experimentation.
|
8
|
+
#
|
9
|
+
# 1. A decycler filter functions the same as a low-pass filter.
|
10
|
+
# 2. A decycler filter is created by subtracting the output of a high-pass filter from the input, thereby removing the
|
11
|
+
# high-frequency components by cancellation.
|
12
|
+
# 3. A decycler filter has very low lag.
|
13
|
+
# 4. A decycler oscillator is created by subtracting the output of a high-pass filter having a shorter cutoff period from the
|
14
|
+
# output of another high-pass filter having a longer cutoff period.
|
15
|
+
# 5. A decycler oscillator shows transitions between uptrends and down-trends at the zero crossings.
|
16
|
+
class DecyclerPoint < IndicatorPoint
|
17
|
+
attr_accessor :decycle, :hp1, :hp2, :osc, :peak, :agc, :ift
|
18
|
+
|
19
|
+
def to_h
|
20
|
+
{
|
21
|
+
"dc" => decycle,
|
22
|
+
"hp1" => hp1,
|
23
|
+
"hp2" => hp2,
|
24
|
+
"osc" => osc,
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize_data_points(indicator:)
|
29
|
+
@decycle = oc2
|
30
|
+
@hp1 = 0.0
|
31
|
+
@hp2 = 0.0
|
32
|
+
@osc = 0.0
|
33
|
+
@peak = 0.0
|
34
|
+
@agc = 0.0
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Decycler < Indicator
|
39
|
+
def max_period
|
40
|
+
dc_period
|
41
|
+
end
|
42
|
+
|
43
|
+
def min_period
|
44
|
+
settings.min_period
|
45
|
+
end
|
46
|
+
|
47
|
+
def compute_decycler
|
48
|
+
alpha = period_to_alpha(max_period)
|
49
|
+
p0.decycle = (alpha / 2) * (p0.oc2 + p1.oc2) + (1.0 - alpha) * p1.decycle
|
50
|
+
end
|
51
|
+
|
52
|
+
# alpha1 = (Cosine(.707*360 / HPPeriod1) + Sine (.707*360 / HPPeriod1) - 1) / Cosine(.707*360 / HPPeriod1);
|
53
|
+
# HP1 = (1 - alpha1 / 2)*(1 - alpha1 / 2)*(Close - 2*Close[1] + Close[2]) + 2*(1 - alpha1)*HP1[1] - (1 - alpha1)*(1 - alpha1)*HP1[2];
|
54
|
+
def compute_hp(period, hp)
|
55
|
+
radians = deg2rad(360)
|
56
|
+
c = Math.cos(0.707 * radians / period)
|
57
|
+
s = Math.sin(0.707 * radians / period)
|
58
|
+
alpha = (c + s - 1) / c
|
59
|
+
(1 - alpha / 2)**2 * (p0.oc2 - 2 * p1.oc2 + p2.oc2) + 2 * (1 - alpha) * p1.send(hp) - (1 - alpha) * (1 - alpha) * p2.send(hp)
|
60
|
+
end
|
61
|
+
|
62
|
+
def compute_oscillator
|
63
|
+
p0.hp1 = compute_hp(min_period, :hp1)
|
64
|
+
p0.hp2 = compute_hp(max_period, :hp2)
|
65
|
+
p0.osc = p0.hp2 - p0.hp1
|
66
|
+
end
|
67
|
+
|
68
|
+
def compute_agc
|
69
|
+
p0.peak = [p0.osc.abs, 0.991 * p1.peak].max
|
70
|
+
p0.agc = p0.peak.zero? ? p0.osc : p0.osc / p0.peak
|
71
|
+
end
|
72
|
+
|
73
|
+
def compute_ift
|
74
|
+
p0.ift = ift(p0.agc, 5.0)
|
75
|
+
end
|
76
|
+
|
77
|
+
def compute
|
78
|
+
compute_decycler
|
79
|
+
compute_oscillator
|
80
|
+
compute_agc
|
81
|
+
compute_ift
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../indicator_point"
|
2
4
|
require_relative "dominant_cycle"
|
3
5
|
|
@@ -22,20 +24,20 @@ module Quant
|
|
22
24
|
attribute :reversal, default: false
|
23
25
|
end
|
24
26
|
|
25
|
-
# Auto-Correlation Reversals
|
27
|
+
# Auto-Correlation Reversals is a method of computing the dominant cycle
|
28
|
+
# by correlating the data stream with itself delayed by a lag.
|
29
|
+
# Construction of the autocorrelation periodogram starts with the
|
30
|
+
# autocorrelation function using the minimum three bars of averaging.
|
31
|
+
# The cyclic information is extracted using a discrete Fourier transform
|
32
|
+
# (DFT) of the autocorrelation results.
|
26
33
|
class Acr < DominantCycle
|
27
|
-
|
28
|
-
|
29
|
-
end
|
30
|
-
|
31
|
-
def bandwidth
|
32
|
-
deg2rad(370)
|
33
|
-
end
|
34
|
+
BANDWIDTH_DEGREES = 370
|
35
|
+
BANDWIDTH_RADIANS = BANDWIDTH_DEGREES * Math::PI / 180.0
|
34
36
|
|
35
37
|
def compute_auto_correlations
|
36
38
|
(min_period..max_period).each do |period|
|
37
39
|
corr = Statistics::Correlation.new
|
38
|
-
|
40
|
+
micro_period.times do |lookback_period|
|
39
41
|
corr.add(p(lookback_period).filter, p(period + lookback_period).filter)
|
40
42
|
end
|
41
43
|
p0.corr[period] = corr.coefficient
|
@@ -46,8 +48,8 @@ module Quant
|
|
46
48
|
p0.maxpwr = 0.995 * p1.maxpwr
|
47
49
|
|
48
50
|
(min_period..max_period).each do |period|
|
49
|
-
(
|
50
|
-
radians =
|
51
|
+
(micro_period..max_period).each do |n|
|
52
|
+
radians = BANDWIDTH_RADIANS * n / period
|
51
53
|
p0.cospart[period] += p0.corr[n] * Math.cos(radians)
|
52
54
|
p0.sinpart[period] += p0.corr[n] * Math.sin(radians)
|
53
55
|
end
|
@@ -98,4 +100,4 @@ module Quant
|
|
98
100
|
end
|
99
101
|
end
|
100
102
|
end
|
101
|
-
end
|
103
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "dominant_cycle"
|
2
4
|
|
3
5
|
module Quant
|
@@ -14,6 +16,11 @@ module Quant
|
|
14
16
|
attribute :direction, default: :flat
|
15
17
|
end
|
16
18
|
|
19
|
+
# The band-pass dominant cycle passes signals within a certain frequency
|
20
|
+
# range, and attenuates signals outside that range.
|
21
|
+
# The trend component of the signal is revoved, leaving only the cyclical
|
22
|
+
# component. Then we count number of iterations between zero crossings
|
23
|
+
# and this is the `period` of the dominant cycle.
|
17
24
|
class BandPass < DominantCycle
|
18
25
|
def bandwidth
|
19
26
|
0.75
|
@@ -1,9 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Quant
|
2
4
|
class Indicators
|
3
5
|
class DominantCycles
|
4
|
-
# The Dual Differentiator algorithm computes the phase angle from the
|
5
|
-
#
|
6
|
-
#
|
6
|
+
# The Dual Differentiator algorithm computes the phase angle from the
|
7
|
+
# analytic signal as the arctangent of the ratio of the imaginary
|
8
|
+
# component to the real component. Further, the angular frequency
|
9
|
+
# is defined as the rate change of phase. We can use these facts to
|
10
|
+
# derive the cycle period.
|
7
11
|
class Differential < DominantCycle
|
8
12
|
def compute_period
|
9
13
|
p0.ddd = (p0.q2 * (p0.i2 - p1.i2)) - (p0.i2 * (p0.q2 - p1.q2))
|
@@ -16,4 +20,4 @@ module Quant
|
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
19
|
-
end
|
23
|
+
end
|
@@ -1,7 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../indicator"
|
2
4
|
|
3
5
|
module Quant
|
4
6
|
class Indicators
|
7
|
+
# Dominant Cycles measure the primary cycle within a given range. By default, the library
|
8
|
+
# is wired to look for cycles between 10 and 48 bars. These values can be adjusted by setting
|
9
|
+
# the `min_period` and `max_period` configuration values in {Quant::Config}.
|
10
|
+
#
|
11
|
+
# Quant.configure_indicators(min_period: 8, max_period: 32)
|
12
|
+
#
|
13
|
+
# The default dominant cycle kind is the `half_period` filter. This can be adjusted by setting
|
14
|
+
# the `dominant_cycle_kind` configuration value in {Quant::Config}.
|
15
|
+
#
|
16
|
+
# Quant.configure_indicators(dominant_cycle_kind: :band_pass)
|
17
|
+
#
|
18
|
+
# The purpose of these indicators is to compute the dominant cycle and underpin the various
|
19
|
+
# indicators that would otherwise be setting an arbitrary lookback period. This makes the
|
20
|
+
# indicators adaptive and auto-tuning to the market dynamics. Or so the theory goes!
|
5
21
|
class DominantCycles
|
6
22
|
class DominantCyclePoint < Quant::Indicators::IndicatorPoint
|
7
23
|
attribute :smooth, default: 0.0
|
@@ -39,6 +55,8 @@ module Quant
|
|
39
55
|
p0.inst_period = p0.inst_period.clamp(min_period, max_period)
|
40
56
|
end
|
41
57
|
|
58
|
+
attr_reader :points
|
59
|
+
|
42
60
|
# constrain magnitude of change in phase
|
43
61
|
def constrain_period_magnitude_change
|
44
62
|
p0.inst_period = [1.5 * p1.inst_period, p0.inst_period].min
|
@@ -60,11 +78,6 @@ module Quant
|
|
60
78
|
[p0.period.to_i, min_period].max
|
61
79
|
end
|
62
80
|
|
63
|
-
def period_points(max_period)
|
64
|
-
extent = [values.size, max_period].min
|
65
|
-
values[-extent, extent]
|
66
|
-
end
|
67
|
-
|
68
81
|
def compute
|
69
82
|
compute_input_data_points
|
70
83
|
compute_quadrature_components
|
@@ -125,4 +138,4 @@ module Quant
|
|
125
138
|
end
|
126
139
|
end
|
127
140
|
end
|
128
|
-
end
|
141
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "dominant_cycle"
|
4
|
+
|
5
|
+
module Quant
|
6
|
+
class Indicators
|
7
|
+
class DominantCycles
|
8
|
+
# This dominant cycle indicator is based on the half period
|
9
|
+
# that is the midpoint of the `min_period` and `max_period`
|
10
|
+
# configured in the `Quant.config.indicators` object.
|
11
|
+
# Effectively providing a static, arbitrarily set period.
|
12
|
+
class HalfPeriodPoint < Quant::Indicators::IndicatorPoint
|
13
|
+
attribute :period, default: :half_period
|
14
|
+
end
|
15
|
+
|
16
|
+
class HalfPeriod < DominantCycle
|
17
|
+
def compute
|
18
|
+
# No-Op
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,11 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../indicator_point"
|
2
4
|
require_relative "dominant_cycle"
|
3
5
|
|
4
6
|
module Quant
|
5
7
|
class Indicators
|
6
8
|
class DominantCycles
|
7
|
-
# Homodyne means the signal is multiplied by itself. More precisely,
|
8
|
-
#
|
9
|
+
# Homodyne means the signal is multiplied by itself. More precisely,
|
10
|
+
# we want to multiply the signal of the current bar with the complex
|
11
|
+
# value of the signal one bar ago
|
9
12
|
class Homodyne < DominantCycle
|
10
13
|
def compute_period
|
11
14
|
p0.re = (p0.i2 * p1.i2) + (p0.q2 * p1.q2)
|
@@ -14,7 +17,7 @@ module Quant
|
|
14
17
|
p0.re = (0.2 * p0.re) + (0.8 * p1.re)
|
15
18
|
p0.im = (0.2 * p0.im) + (0.8 * p1.im)
|
16
19
|
|
17
|
-
p0.inst_period = 360.0 / rad2deg(Math.atan(p0.im/p0.re)) if (p0.im != 0) && (p0.re != 0)
|
20
|
+
p0.inst_period = 360.0 / rad2deg(Math.atan(p0.im / p0.re)) if (p0.im != 0) && (p0.re != 0)
|
18
21
|
|
19
22
|
constrain_period_magnitude_change
|
20
23
|
constrain_period_bars
|
@@ -24,4 +27,4 @@ module Quant
|
|
24
27
|
end
|
25
28
|
end
|
26
29
|
end
|
27
|
-
end
|
30
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "dominant_cycle"
|
2
4
|
|
3
5
|
module Quant
|
@@ -8,7 +10,7 @@ module Quant
|
|
8
10
|
# at each sample by taking the arctangent of the ratio of the quadrature
|
9
11
|
# component to the in-phase component. A delta phase is generated by
|
10
12
|
# taking the difference of the phase between successive samples.
|
11
|
-
# At each
|
13
|
+
# At each sample we can then look backwards, adding up the delta
|
12
14
|
# phases. When the sum of the delta phases reaches 360 degrees,
|
13
15
|
# we must have passed through one full cycle, on average. The process
|
14
16
|
# is repeated for each new sample.
|
@@ -30,10 +32,12 @@ module Quant
|
|
30
32
|
|
31
33
|
p0.accumulator_phase = Math.atan(p0.q1 / p0.i1) unless p0.i1.zero?
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
35
|
+
if p0.i1 < 0 && p0.q1 > 0
|
36
|
+
p0.accumulator_phase = 180.0 - p0.accumulator_phase
|
37
|
+
elsif p0.i1 < 0 && p0.q1 < 0
|
38
|
+
p0.accumulator_phase = 180.0 + p0.accumulator_phase
|
39
|
+
elsif p0.i1 > 0 && p0.q1 < 0
|
40
|
+
p0.accumulator_phase = 360.0 - p0.accumulator_phase
|
37
41
|
end
|
38
42
|
|
39
43
|
p0.delta_phase = p1.accumulator_phase - p0.accumulator_phase
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class Indicators
|
5
|
+
class FramaPoint < IndicatorPoint
|
6
|
+
attribute :frama, default: :input
|
7
|
+
end
|
8
|
+
|
9
|
+
# FRAMA (FRactal Adaptive Moving Average). A nonlinear moving average
|
10
|
+
# is derived using the Hurst exponent. It rapidly follows significant
|
11
|
+
# changes in price but becomes very flat in congestion zones so that
|
12
|
+
# bad whipsaw trades can be eliminated.
|
13
|
+
#
|
14
|
+
# SOURCE: http://www.mesasoftware.com/papers/FRAMA.pdf
|
15
|
+
class Frama < Indicator
|
16
|
+
using Quant
|
17
|
+
|
18
|
+
# The max_period is divided into two smaller, equal periods, so must be even
|
19
|
+
def max_period
|
20
|
+
@max_period ||= begin
|
21
|
+
mp = super
|
22
|
+
mp.even? ? mp : mp + 1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# def max_period
|
27
|
+
# mp = dc_period
|
28
|
+
# mp.even? ? mp : mp + 1
|
29
|
+
# end
|
30
|
+
|
31
|
+
def half_period
|
32
|
+
max_period / 2
|
33
|
+
end
|
34
|
+
|
35
|
+
def compute
|
36
|
+
pp = period_points(max_period).map(&:input)
|
37
|
+
return if pp.size < max_period
|
38
|
+
|
39
|
+
n3 = (pp.maximum - pp.minimum) / max_period
|
40
|
+
|
41
|
+
ppn2 = pp.first(half_period)
|
42
|
+
n2 = (ppn2.maximum - ppn2.minimum) / half_period
|
43
|
+
|
44
|
+
ppn1 = pp.last(half_period)
|
45
|
+
n1 = (ppn1.maximum - ppn1.minimum) / half_period
|
46
|
+
|
47
|
+
dimension = (Math.log(n1 + n2) - Math.log(n3)) / Math.log(2)
|
48
|
+
alpha = Math.exp(-4.6 * (dimension - 1.0)).clamp(0.01, 1.0)
|
49
|
+
p0.frama = (alpha * p0.input) + ((1 - alpha) * p1.frama)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -19,6 +19,7 @@ module Quant
|
|
19
19
|
@series = series
|
20
20
|
@source = source
|
21
21
|
@points = {}
|
22
|
+
series.new_indicator(self)
|
22
23
|
series.each { |tick| self << tick }
|
23
24
|
end
|
24
25
|
|
@@ -46,6 +47,14 @@ module Quant
|
|
46
47
|
Quant.config.indicators.pivot_kind
|
47
48
|
end
|
48
49
|
|
50
|
+
def dominant_cycle
|
51
|
+
series.indicators[source].dominant_cycle
|
52
|
+
end
|
53
|
+
|
54
|
+
def dc_period
|
55
|
+
dominant_cycle.points[t0].period
|
56
|
+
end
|
57
|
+
|
49
58
|
def ticks
|
50
59
|
@points.keys
|
51
60
|
end
|
@@ -62,6 +71,11 @@ module Quant
|
|
62
71
|
@points.size
|
63
72
|
end
|
64
73
|
|
74
|
+
def period_points(max_period)
|
75
|
+
extent = [values.size, max_period].min
|
76
|
+
values[-extent, extent]
|
77
|
+
end
|
78
|
+
|
65
79
|
attr_reader :p0, :p1, :p2, :p3
|
66
80
|
attr_reader :t0, :t1, :t2, :t3
|
67
81
|
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class Indicators
|
5
|
+
class MamaPoint < IndicatorPoint
|
6
|
+
attribute :smooth, default: 0.0
|
7
|
+
attribute :detrend, default: 0.0
|
8
|
+
attribute :re, default: 0.0
|
9
|
+
attribute :im, default: 0.0
|
10
|
+
attribute :i1, default: 0.0
|
11
|
+
attribute :q1, default: 0.0
|
12
|
+
attribute :ji, default: 0.0
|
13
|
+
attribute :jq, default: 0.0
|
14
|
+
attribute :i2, default: 0.0
|
15
|
+
attribute :q2, default: 0.0
|
16
|
+
attribute :period, default: :min_period
|
17
|
+
attribute :smooth_period, default: :min_period
|
18
|
+
attribute :mama, default: :input
|
19
|
+
attribute :fama, default: :input
|
20
|
+
attribute :gama, default: :input
|
21
|
+
attribute :dama, default: :input
|
22
|
+
attribute :lama, default: :input
|
23
|
+
attribute :faga, default: :input
|
24
|
+
attribute :phase, default: 0.0
|
25
|
+
attribute :delta_phase, default: 0.0
|
26
|
+
attribute :osc, default: 0.0
|
27
|
+
attribute :crossed, default: :unchanged
|
28
|
+
|
29
|
+
def crossed_up?
|
30
|
+
@crossed == :up
|
31
|
+
end
|
32
|
+
|
33
|
+
def crossed_down?
|
34
|
+
@crossed == :down
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# https://www.mesasoftware.com/papers/MAMA.pdf
|
39
|
+
# MESA Adaptive Moving Average (MAMA) adapts to price movement in an
|
40
|
+
# entirely new and unique way. The adapation is based on the rate change
|
41
|
+
# of phase as measured by the Hilbert Transform Discriminator.
|
42
|
+
#
|
43
|
+
# This version of Ehler's MAMA indicator duplicates the computations
|
44
|
+
# present in the homodyne version of the dominant cycle indicator.
|
45
|
+
# Use this version of the indicator when you're using a different
|
46
|
+
# dominant cycle indicator other than the homodyne for the rest
|
47
|
+
# of your indicators.
|
48
|
+
class Mama < Indicator
|
49
|
+
# constrain between 6 and 50 bars
|
50
|
+
def constrain_period_bars
|
51
|
+
p0.period = p0.period.clamp(min_period, max_period)
|
52
|
+
end
|
53
|
+
|
54
|
+
# constrain magnitude of change in phase
|
55
|
+
def constrain_period_magnitude_change
|
56
|
+
p0.period = [1.5 * p1.period, p0.period].min
|
57
|
+
p0.period = [0.67 * p1.period, p0.period].max
|
58
|
+
end
|
59
|
+
|
60
|
+
# amplitude correction using previous period value
|
61
|
+
def compute_smooth_period
|
62
|
+
p0.period = ((0.2 * p0.period) + (0.8 * p1.period)).round
|
63
|
+
p0.smooth_period = ((0.33333 * p0.period) + (0.666667 * p1.smooth_period)).round
|
64
|
+
end
|
65
|
+
|
66
|
+
def homodyne_discriminator
|
67
|
+
p0.re = (p0.i2 * p1.i2) + (p0.q2 * p1.q2)
|
68
|
+
p0.im = (p0.i2 * p1.q2) - (p0.q2 * p1.i2)
|
69
|
+
|
70
|
+
p0.re = (0.2 * p0.re) + (0.8 * p1.re)
|
71
|
+
p0.im = (0.2 * p0.im) + (0.8 * p1.im)
|
72
|
+
|
73
|
+
p0.period = 360.0 / rad2deg(Math.atan(p0.im / p0.re)) if (p0.im != 0) && (p0.re != 0)
|
74
|
+
|
75
|
+
constrain_period_magnitude_change
|
76
|
+
constrain_period_bars
|
77
|
+
compute_smooth_period
|
78
|
+
end
|
79
|
+
|
80
|
+
def compute_dominant_cycle
|
81
|
+
p0.smooth = wma :input
|
82
|
+
p0.detrend = hilbert_transform :smooth, period: p1.period
|
83
|
+
|
84
|
+
# { Compute Inphase and Quadrature components }
|
85
|
+
p0.q1 = hilbert_transform :detrend, period: p1.period
|
86
|
+
p0.i1 = p3.detrend
|
87
|
+
|
88
|
+
# { Advance the phase of I1 and Q1 by 90 degrees }
|
89
|
+
p0.ji = hilbert_transform :i1, period: p1.period
|
90
|
+
p0.jq = hilbert_transform :q1, period: p1.period
|
91
|
+
|
92
|
+
# { Smooth the I and Q components before applying the discriminator }
|
93
|
+
p0.i2 = (0.2 * (p0.i1 - p0.jq)) + 0.8 * (p1.i2 || (p0.i1 - p0.jq))
|
94
|
+
p0.q2 = (0.2 * (p0.q1 + p0.ji)) + 0.8 * (p1.q2 || (p0.q1 + p0.ji))
|
95
|
+
|
96
|
+
homodyne_discriminator
|
97
|
+
end
|
98
|
+
|
99
|
+
def fast_limit
|
100
|
+
@fast_limit ||= bars_to_alpha(min_period / 2)
|
101
|
+
end
|
102
|
+
|
103
|
+
def slow_limit
|
104
|
+
@slow_limit ||= bars_to_alpha(max_period)
|
105
|
+
end
|
106
|
+
|
107
|
+
def compute_dominant_cycle_phase
|
108
|
+
p0.delta_phase = p1.phase - p0.phase
|
109
|
+
p0.delta_phase = 1.0 if p0.delta_phase < 1.0
|
110
|
+
end
|
111
|
+
|
112
|
+
FAMA = 0.500
|
113
|
+
GAMA = 0.950
|
114
|
+
DAMA = 0.125
|
115
|
+
LAMA = 0.100
|
116
|
+
FAGA = 0.050
|
117
|
+
|
118
|
+
def compute_moving_averages
|
119
|
+
alpha = [fast_limit / p0.delta_phase, slow_limit].max
|
120
|
+
p0.mama = (alpha * p0.input) + ((1.0 - alpha) * p1.mama)
|
121
|
+
|
122
|
+
p0.fama = (FAMA * alpha * p0.mama) + ((1.0 - (FAMA * alpha)) * p1.fama)
|
123
|
+
p0.gama = (GAMA * alpha * p0.mama) + ((1.0 - (GAMA * alpha)) * p1.gama)
|
124
|
+
p0.dama = (DAMA * alpha * p0.mama) + ((1.0 - (DAMA * alpha)) * p1.dama)
|
125
|
+
p0.lama = (LAMA * alpha * p0.mama) + ((1.0 - (LAMA * alpha)) * p1.lama)
|
126
|
+
p0.faga = (FAGA * alpha * p0.fama) + ((1.0 - (FAGA * alpha)) * p1.faga)
|
127
|
+
end
|
128
|
+
|
129
|
+
def compute_oscillator
|
130
|
+
p0.osc = p0.mama - p0.fama
|
131
|
+
p0.crossed = :up if p0.osc >= 0 && p1.osc < 0
|
132
|
+
p0.crossed = :down if p0.osc <= 0 && p1.osc > 0
|
133
|
+
end
|
134
|
+
|
135
|
+
def compute
|
136
|
+
compute_dominant_cycle
|
137
|
+
compute_dominant_cycle_phase
|
138
|
+
compute_moving_averages
|
139
|
+
compute_oscillator
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class Indicators
|
5
|
+
# The MESA inidicator
|
6
|
+
class MesaPoint < IndicatorPoint
|
7
|
+
attribute :mama, default: :input
|
8
|
+
attribute :fama, default: :input
|
9
|
+
attribute :dama, default: :input
|
10
|
+
attribute :gama, default: :input
|
11
|
+
attribute :lama, default: :input
|
12
|
+
attribute :faga, default: :input
|
13
|
+
attribute :osc, default: 0.0
|
14
|
+
attribute :crossed, default: :unchanged
|
15
|
+
|
16
|
+
def crossed_up?
|
17
|
+
@crossed == :up
|
18
|
+
end
|
19
|
+
|
20
|
+
def crossed_down?
|
21
|
+
@crossed == :down
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# https://www.mesasoftware.com/papers/MAMA.pdf
|
26
|
+
# MESA Adaptive Moving Average (MAMA) adapts to price movement in an
|
27
|
+
# entirely new and unique way. The adapation is based on the rate change
|
28
|
+
# of phase as measured by the Hilbert Transform Discriminator.
|
29
|
+
#
|
30
|
+
# This version of Ehler's MAMA indicator ties into the homodyne
|
31
|
+
# dominant cycle indicator to provide a more efficient computation
|
32
|
+
# for this indicator. If you're using the homodyne in all your
|
33
|
+
# indicators for the dominant cycle, then this version is useful
|
34
|
+
# as it avoids extra computational steps.
|
35
|
+
class Mesa < Indicator
|
36
|
+
def period
|
37
|
+
dc_period
|
38
|
+
end
|
39
|
+
|
40
|
+
def fast_limit
|
41
|
+
@fast_limit ||= bars_to_alpha(min_period / 2)
|
42
|
+
end
|
43
|
+
|
44
|
+
def slow_limit
|
45
|
+
@slow_limit ||= bars_to_alpha(max_period)
|
46
|
+
end
|
47
|
+
|
48
|
+
def homodyne_dominant_cycle
|
49
|
+
series.indicators[source].dominant_cycles.homodyne
|
50
|
+
end
|
51
|
+
|
52
|
+
def current_dominant_cycle
|
53
|
+
homodyne_dominant_cycle.points[t0]
|
54
|
+
end
|
55
|
+
|
56
|
+
def delta_phase
|
57
|
+
current_dominant_cycle.delta_phase
|
58
|
+
end
|
59
|
+
|
60
|
+
FAMA = 0.500
|
61
|
+
GAMA = 0.950
|
62
|
+
DAMA = 0.125
|
63
|
+
LAMA = 0.100
|
64
|
+
FAGA = 0.050
|
65
|
+
|
66
|
+
def compute
|
67
|
+
alpha = [fast_limit / delta_phase, slow_limit].max
|
68
|
+
|
69
|
+
p0.mama = (alpha * p0.input) + ((1.0 - alpha) * p1.mama)
|
70
|
+
p0.fama = (FAMA * alpha * p0.mama) + ((1.0 - (FAMA * alpha)) * p1.fama)
|
71
|
+
p0.gama = (GAMA * alpha * p0.mama) + ((1.0 - (GAMA * alpha)) * p1.gama)
|
72
|
+
p0.dama = (DAMA * alpha * p0.mama) + ((1.0 - (DAMA * alpha)) * p1.dama)
|
73
|
+
p0.lama = (LAMA * alpha * p0.mama) + ((1.0 - (LAMA * alpha)) * p1.lama)
|
74
|
+
p0.faga = (FAGA * alpha * p0.fama) + ((1.0 - (FAGA * alpha)) * p1.faga)
|
75
|
+
|
76
|
+
compute_oscillator
|
77
|
+
end
|
78
|
+
|
79
|
+
def compute_oscillator
|
80
|
+
p0.osc = p0.mama - p0.fama
|
81
|
+
p0.crossed = :up if p0.osc >= 0 && p1.osc < 0
|
82
|
+
p0.crossed = :down if p0.osc <= 0 && p1.osc > 0
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/quant/indicators.rb
CHANGED
@@ -6,9 +6,13 @@ module Quant
|
|
6
6
|
# used outside those shipped with the library.
|
7
7
|
class Indicators < IndicatorsProxy
|
8
8
|
def ping; indicator(Indicators::Ping) end
|
9
|
+
def adx; indicator(Indicators::Adx) end
|
10
|
+
def atr; indicator(Indicators::Atr) end
|
11
|
+
def mesa; indicator(Indicators::Mesa) end
|
12
|
+
def mama; indicator(Indicators::MAMA) end
|
9
13
|
|
10
14
|
def dominant_cycles
|
11
|
-
@dominant_cycles ||=
|
15
|
+
@dominant_cycles ||= Quant::DominantCycleIndicators.new(series:, source:)
|
12
16
|
end
|
13
17
|
end
|
14
18
|
end
|
@@ -13,12 +13,21 @@ module Quant
|
|
13
13
|
# By design, the {Quant::Indicator} class holds the {Quant::Ticks::Tick} instance
|
14
14
|
# alongside the indicator's computed values for that tick.
|
15
15
|
class IndicatorsProxy
|
16
|
-
attr_reader :series, :source, :indicators
|
16
|
+
attr_reader :series, :source, :dominant_cycle, :indicators
|
17
17
|
|
18
18
|
def initialize(series:, source:)
|
19
19
|
@series = series
|
20
20
|
@source = source
|
21
21
|
@indicators = {}
|
22
|
+
@dominant_cycle = dominant_cycle_indicator
|
23
|
+
end
|
24
|
+
|
25
|
+
def dominant_cycle_indicator
|
26
|
+
kind = Quant.config.indicators.dominant_cycle_kind.to_s
|
27
|
+
base_class_name = kind.split("_").map(&:capitalize).join
|
28
|
+
class_name = "Quant::Indicators::DominantCycles::#{base_class_name}"
|
29
|
+
indicator_class = Object.const_get(class_name)
|
30
|
+
indicator_class.new(series:, source:)
|
22
31
|
end
|
23
32
|
|
24
33
|
# Instantiates the indicator class and stores it in the indicators hash. Once
|
@@ -36,6 +45,7 @@ module Quant
|
|
36
45
|
# The IndicatorsProxy class is not responsible for enforcing
|
37
46
|
# this order of events.
|
38
47
|
def <<(tick)
|
48
|
+
dominant_cycle << tick
|
39
49
|
indicators.each_value { |indicator| indicator << tick }
|
40
50
|
end
|
41
51
|
|
@@ -7,6 +7,16 @@ module Quant
|
|
7
7
|
@indicator_sources = {}
|
8
8
|
end
|
9
9
|
|
10
|
+
def new_indicator(indicator)
|
11
|
+
@indicator_sources[indicator.source] ||= Indicators.new(series: @series, source: indicator.source)
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](source)
|
15
|
+
return @indicator_sources[source] if @indicator_sources.key?(source)
|
16
|
+
|
17
|
+
raise Quant::Errors::InvalidIndicatorSource, "Invalid source, #{source.inspect}."
|
18
|
+
end
|
19
|
+
|
10
20
|
def <<(tick)
|
11
21
|
@indicator_sources.each_value { |indicator| indicator << tick }
|
12
22
|
end
|
@@ -16,7 +16,7 @@ module Quant
|
|
16
16
|
def stochastic(source, period:)
|
17
17
|
subset = values.last(period).map{ |p| p.send(source) }
|
18
18
|
|
19
|
-
lowest, highest = subset.minimum, subset.maximum
|
19
|
+
lowest, highest = subset.minimum.to_f, subset.maximum.to_f
|
20
20
|
return 0.0 if (highest - lowest).zero?
|
21
21
|
|
22
22
|
100.0 * (subset[-1] - lowest) / (highest - lowest)
|
@@ -2,8 +2,17 @@
|
|
2
2
|
|
3
3
|
module Quant
|
4
4
|
module Mixins
|
5
|
+
# Super Smoother Filters provide a way to smooth out the noise in a series
|
6
|
+
# without out introducing undesirable lag that you would other get with
|
7
|
+
# traditional moving averages.
|
8
|
+
#
|
9
|
+
# The EMA only reduces the amplitude at the Nyquist frequency by 13 dB.
|
10
|
+
# On the other hand, the SuperSmoother filter theoretically completely
|
11
|
+
# eliminates components at the Nyquist Frequency. The added benefit is
|
12
|
+
# that the SuperSmoother filter has significantly less lag than the EMA.
|
5
13
|
module SuperSmoother
|
6
|
-
|
14
|
+
# https://www.mesasoftware.com/papers/PredictiveIndicators.pdf
|
15
|
+
def two_pole_super_smooth(source, period:, previous:)
|
7
16
|
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
8
17
|
|
9
18
|
radians = Math.sqrt(2) * Math::PI / period
|
@@ -23,7 +32,7 @@ module Quant
|
|
23
32
|
alias super_smoother two_pole_super_smooth
|
24
33
|
alias ss2p two_pole_super_smooth
|
25
34
|
|
26
|
-
def three_pole_super_smooth(source, period:, previous:
|
35
|
+
def three_pole_super_smooth(source, period:, previous:)
|
27
36
|
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
28
37
|
|
29
38
|
radians = Math::PI / period
|
data/lib/quant/series.rb
CHANGED
@@ -101,6 +101,20 @@ module Quant
|
|
101
101
|
"#<#{self.class.name} symbol=#{symbol} interval=#{interval} ticks=#{ticks.size}>"
|
102
102
|
end
|
103
103
|
|
104
|
+
# When the first indicator is instantiated, it will also lead to instantiating
|
105
|
+
# the dominant cycle indicator. The `new_indicator_lock` prevents reentrant calls
|
106
|
+
# to the `new_indicator` method with infinite recursion.
|
107
|
+
def new_indicator(indicator)
|
108
|
+
return if @new_indicator_lock
|
109
|
+
|
110
|
+
begin
|
111
|
+
@new_indicator_lock = true
|
112
|
+
indicators.new_indicator(indicator)
|
113
|
+
ensure
|
114
|
+
@new_indicator_lock = false
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
104
118
|
def <<(tick)
|
105
119
|
tick = Ticks::Spot.new(price: tick) if tick.is_a?(Numeric)
|
106
120
|
indicators << tick unless tick.series?
|
@@ -8,14 +8,16 @@ module Quant
|
|
8
8
|
# papers and books on the subject of technical analysis by John Ehlers where he variously suggests
|
9
9
|
# a minimum period of 8 or 10 and a max period of 48.
|
10
10
|
#
|
11
|
-
# The half period is the average of the max_period and min_period.
|
11
|
+
# The half period is the average of the max_period and min_period. It is read-only and always computed
|
12
|
+
# relative to `min_period` and `max_period`.
|
13
|
+
#
|
12
14
|
# The micro period comes from Ehler's writings on Swami charts and auto-correlation computations, which
|
13
15
|
# is a period of 3 bars. It is useful enough in various indicators to be its own setting.
|
14
16
|
#
|
15
17
|
# The dominant cycle kind is the kind of dominant cycle to use in the indicator. The default is +:settings+
|
16
18
|
# which means the dominant cycle is whatever the +max_period+ is set to. It is not adaptive when configured
|
17
19
|
# this way. The other kinds are adaptive and are computed from the series data. The choices are:
|
18
|
-
# * +:
|
20
|
+
# * +:half_period+ - the half_period is the dominant cycle and is not adaptive
|
19
21
|
# * +:band_pass+ - The zero crossings of the band pass filter are used to compute the dominant cycle
|
20
22
|
# * +:auto_correlation_reversal+ - The dominant cycle is computed from the auto-correlation of the series.
|
21
23
|
# * +:homodyne+ - The dominant cycle is computed from the homodyne discriminator.
|
@@ -48,8 +50,8 @@ module Quant
|
|
48
50
|
new
|
49
51
|
end
|
50
52
|
|
51
|
-
|
52
|
-
attr_accessor :dominant_cycle_kind, :pivot_kind
|
53
|
+
attr_reader :max_period, :min_period, :half_period
|
54
|
+
attr_accessor :micro_period, :dominant_cycle_kind, :pivot_kind
|
53
55
|
|
54
56
|
def initialize(**settings)
|
55
57
|
@max_period = settings[:max_period] || Settings::MAX_PERIOD
|
@@ -64,14 +66,22 @@ module Quant
|
|
64
66
|
def apply_settings(**settings)
|
65
67
|
@max_period = settings.fetch(:max_period, @max_period)
|
66
68
|
@min_period = settings.fetch(:min_period, @min_period)
|
67
|
-
|
69
|
+
compute_half_period
|
68
70
|
@micro_period = settings.fetch(:micro_period, @micro_period)
|
69
71
|
@dominant_cycle_kind = settings.fetch(:dominant_cycle_kind, @dominant_cycle_kind)
|
70
72
|
@pivot_kind = settings.fetch(:pivot_kind, @pivot_kind)
|
71
73
|
end
|
72
74
|
|
75
|
+
def max_period=(value)
|
76
|
+
(@max_period = value).tap { compute_half_period }
|
77
|
+
end
|
78
|
+
|
79
|
+
def min_period=(value)
|
80
|
+
(@min_period = value).tap { compute_half_period }
|
81
|
+
end
|
82
|
+
|
73
83
|
def compute_half_period
|
74
|
-
(max_period + min_period) / 2
|
84
|
+
@half_period = (max_period + min_period) / 2
|
75
85
|
end
|
76
86
|
end
|
77
87
|
end
|
data/lib/quant/settings.rb
CHANGED
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.2.
|
4
|
+
version: 0.2.2
|
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-03-
|
11
|
+
date: 2024-03-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|
@@ -48,18 +48,26 @@ files:
|
|
48
48
|
- lib/quant/asset_class.rb
|
49
49
|
- lib/quant/attributes.rb
|
50
50
|
- lib/quant/config.rb
|
51
|
+
- lib/quant/dominant_cycle_indicators.rb
|
51
52
|
- lib/quant/errors.rb
|
52
53
|
- lib/quant/experimental.rb
|
53
54
|
- lib/quant/indicators.rb
|
54
|
-
- lib/quant/indicators/
|
55
|
+
- lib/quant/indicators/adx.rb
|
56
|
+
- lib/quant/indicators/atr.rb
|
57
|
+
- lib/quant/indicators/cci.rb
|
58
|
+
- lib/quant/indicators/decycler.rb
|
55
59
|
- lib/quant/indicators/dominant_cycles/acr.rb
|
56
60
|
- lib/quant/indicators/dominant_cycles/band_pass.rb
|
57
61
|
- lib/quant/indicators/dominant_cycles/differential.rb
|
58
62
|
- lib/quant/indicators/dominant_cycles/dominant_cycle.rb
|
63
|
+
- lib/quant/indicators/dominant_cycles/half_period.rb
|
59
64
|
- lib/quant/indicators/dominant_cycles/homodyne.rb
|
60
65
|
- lib/quant/indicators/dominant_cycles/phase_accumulator.rb
|
66
|
+
- lib/quant/indicators/frama.rb
|
61
67
|
- lib/quant/indicators/indicator.rb
|
62
68
|
- lib/quant/indicators/indicator_point.rb
|
69
|
+
- lib/quant/indicators/mama.rb
|
70
|
+
- lib/quant/indicators/mesa.rb
|
63
71
|
- lib/quant/indicators/ping.rb
|
64
72
|
- lib/quant/indicators_proxy.rb
|
65
73
|
- lib/quant/indicators_sources.rb
|
@@ -1,10 +0,0 @@
|
|
1
|
-
module Quant
|
2
|
-
class DominantCycleIndicators < IndicatorsProxy
|
3
|
-
def acr; indicator(Indicators::DominantCycles::Acr) end
|
4
|
-
def band_pass; indicator(Indicators::DominantCycles::BandPass) end
|
5
|
-
def homodyne; indicator(Indicators::DominantCycles::Homodyne) end
|
6
|
-
|
7
|
-
def differential; indicator(Indicators::DominantCycles::Differential) end
|
8
|
-
def phase_accumulator; indicator(Indicators::DominantCycles::PhaseAccumulator) end
|
9
|
-
end
|
10
|
-
end
|