quantitative 0.2.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf0aa73684247efc7bc9750c27c856a094fa362d99c1eba73bd70457da8f3a97
4
- data.tar.gz: 56dcaa82230328cb44149ceb957420484217e33c6f601ea135997391bfb95440
3
+ metadata.gz: 6b5786b75635cde82e7919fce6b8fcd568724b27fc2810efe6db36a7e6dd09cc
4
+ data.tar.gz: 3a6fb86c25bb4f2b1e4994710539bfb528377ef57531e6845a4417c87fc86e29
5
5
  SHA512:
6
- metadata.gz: b674a5e4408a69039cf2c7ad2378d065e3f88b409a7530108af1bfc5a186a02690cba8ffb1c8d40a87ad69368f302d9eec0211135beec7db6a9c51c4e1be46d3
7
- data.tar.gz: e7d7693cd878ebe777f81ca0d90b4f305e645c8547aa9f420cccb4a4f9b53920211a2b7f49af146508756e6e38f5cc7e0c4b57f1c8abdd722f65bd3409abd02f
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's dependencies in quantitative.gemspec
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 'rspec-github'
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.1)
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
@@ -1,3 +1,6 @@
1
+
2
+ require_relative 'indicators_proxy'
3
+
1
4
  module Quant
2
5
  # Dominant Cycles measure the primary cycle within a given range. By default, the library
3
6
  # is wired to look for cycles between 10 and 48 bars. These values can be adjusted by setting
@@ -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
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "dominant_cycle"
2
4
 
3
5
  module Quant
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quant
2
4
  class Indicators
3
5
  class DominantCycles
@@ -18,4 +20,4 @@ module Quant
18
20
  end
19
21
  end
20
22
  end
21
- end
23
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../indicator"
2
4
 
3
5
  module Quant
@@ -76,11 +78,6 @@ module Quant
76
78
  [p0.period.to_i, min_period].max
77
79
  end
78
80
 
79
- def period_points(max_period)
80
- extent = [values.size, max_period].min
81
- values[-extent, extent]
82
- end
83
-
84
81
  def compute
85
82
  compute_input_data_points
86
83
  compute_quadrature_components
@@ -141,4 +138,4 @@ module Quant
141
138
  end
142
139
  end
143
140
  end
144
- end
141
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "dominant_cycle"
2
4
 
3
5
  module Quant
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../indicator_point"
2
4
  require_relative "dominant_cycle"
3
5
 
@@ -15,7 +17,7 @@ module Quant
15
17
  p0.re = (0.2 * p0.re) + (0.8 * p1.re)
16
18
  p0.im = (0.2 * p0.im) + (0.8 * p1.im)
17
19
 
18
- 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)
19
21
 
20
22
  constrain_period_magnitude_change
21
23
  constrain_period_bars
@@ -25,4 +27,4 @@ module Quant
25
27
  end
26
28
  end
27
29
  end
28
- 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
@@ -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
- case
34
- when p0.i1 < 0 && p0.q1 > 0 then p0.accumulator_phase = 180.0 - p0.accumulator_phase
35
- when p0.i1 < 0 && p0.q1 < 0 then p0.accumulator_phase = 180.0 + p0.accumulator_phase
36
- when p0.i1 > 0 && p0.q1 < 0 then p0.accumulator_phase = 360.0 - p0.accumulator_phase
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
@@ -71,6 +71,11 @@ module Quant
71
71
  @points.size
72
72
  end
73
73
 
74
+ def period_points(max_period)
75
+ extent = [values.size, max_period].min
76
+ values[-extent, extent]
77
+ end
78
+
74
79
  attr_reader :p0, :p1, :p2, :p3
75
80
  attr_reader :t0, :t1, :t2, :t3
76
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
@@ -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 ||= Indicators::DominantCycleIndicators.new(series:, source:)
15
+ @dominant_cycles ||= Quant::DominantCycleIndicators.new(series:, source:)
12
16
  end
13
17
  end
14
18
  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
- def two_pole_super_smooth(source, period:, previous: :ss)
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: :ss)
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/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quant
4
- VERSION = "0.2.1"
4
+ VERSION = "0.2.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quantitative
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
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-16 00:00:00.000000000 Z
11
+ date: 2024-03-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -48,10 +48,14 @@ 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/dominant_cycle_indicators.rb
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
@@ -59,8 +63,11 @@ files:
59
63
  - lib/quant/indicators/dominant_cycles/half_period.rb
60
64
  - lib/quant/indicators/dominant_cycles/homodyne.rb
61
65
  - lib/quant/indicators/dominant_cycles/phase_accumulator.rb
66
+ - lib/quant/indicators/frama.rb
62
67
  - lib/quant/indicators/indicator.rb
63
68
  - lib/quant/indicators/indicator_point.rb
69
+ - lib/quant/indicators/mama.rb
70
+ - lib/quant/indicators/mesa.rb
64
71
  - lib/quant/indicators/ping.rb
65
72
  - lib/quant/indicators_proxy.rb
66
73
  - lib/quant/indicators_sources.rb