quantitative 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +9 -4
  3. data/Gemfile.lock +9 -1
  4. data/README.md +5 -0
  5. data/lib/quant/{indicators/dominant_cycle_indicators.rb → dominant_cycles_source.rb} +19 -8
  6. data/lib/quant/experimental.rb +8 -1
  7. data/lib/quant/indicators/adx.rb +83 -0
  8. data/lib/quant/indicators/atr.rb +79 -0
  9. data/lib/quant/indicators/cci.rb +63 -0
  10. data/lib/quant/indicators/decycler.rb +71 -0
  11. data/lib/quant/indicators/dominant_cycles/acr.rb +2 -0
  12. data/lib/quant/indicators/dominant_cycles/band_pass.rb +2 -0
  13. data/lib/quant/indicators/dominant_cycles/differential.rb +3 -1
  14. data/lib/quant/indicators/dominant_cycles/dominant_cycle.rb +12 -6
  15. data/lib/quant/indicators/dominant_cycles/half_period.rb +2 -0
  16. data/lib/quant/indicators/dominant_cycles/homodyne.rb +4 -2
  17. data/lib/quant/indicators/dominant_cycles/phase_accumulator.rb +8 -4
  18. data/lib/quant/indicators/frama.rb +50 -0
  19. data/lib/quant/indicators/indicator.rb +50 -2
  20. data/lib/quant/indicators/mama.rb +143 -0
  21. data/lib/quant/indicators/mesa.rb +86 -0
  22. data/lib/quant/indicators/pivot.rb +107 -0
  23. data/lib/quant/indicators/pivots/atr.rb +41 -0
  24. data/lib/quant/indicators/pivots/bollinger.rb +45 -0
  25. data/lib/quant/indicators/pivots/camarilla.rb +61 -0
  26. data/lib/quant/indicators/pivots/classic.rb +24 -0
  27. data/lib/quant/indicators/pivots/demark.rb +50 -0
  28. data/lib/quant/indicators/pivots/donchian.rb +40 -0
  29. data/lib/quant/indicators/pivots/fibbonacci.rb +22 -0
  30. data/lib/quant/indicators/pivots/guppy.rb +39 -0
  31. data/lib/quant/indicators/pivots/keltner.rb +43 -0
  32. data/lib/quant/indicators/pivots/murrey.rb +34 -0
  33. data/lib/quant/indicators/pivots/traditional.rb +36 -0
  34. data/lib/quant/indicators/pivots/woodie.rb +59 -0
  35. data/lib/quant/indicators_source.rb +140 -0
  36. data/lib/quant/indicators_sources.rb +36 -10
  37. data/lib/quant/mixins/stochastic.rb +1 -1
  38. data/lib/quant/mixins/super_smoother.rb +11 -2
  39. data/lib/quant/pivots_source.rb +28 -0
  40. data/lib/quant/refinements/array.rb +14 -0
  41. data/lib/quant/series.rb +8 -19
  42. data/lib/quant/settings/indicators.rb +11 -0
  43. data/lib/quant/version.rb +1 -1
  44. data/possibilities.png +0 -0
  45. data/quantitative.gemspec +39 -0
  46. metadata +27 -5
  47. data/lib/quant/indicators.rb +0 -14
  48. data/lib/quant/indicators_proxy.rb +0 -68
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf0aa73684247efc7bc9750c27c856a094fa362d99c1eba73bd70457da8f3a97
4
- data.tar.gz: 56dcaa82230328cb44149ceb957420484217e33c6f601ea135997391bfb95440
3
+ metadata.gz: '06693d3511af25d58463fd872f004265889bb0c8d8344dd4fc8436bd4a66f63b'
4
+ data.tar.gz: b1e7db72820532999567608e1d3b864a7e4b1029b363de4c05c1032b177abc5a
5
5
  SHA512:
6
- metadata.gz: b674a5e4408a69039cf2c7ad2378d065e3f88b409a7530108af1bfc5a186a02690cba8ffb1c8d40a87ad69368f302d9eec0211135beec7db6a9c51c4e1be46d3
7
- data.tar.gz: e7d7693cd878ebe777f81ca0d90b4f305e645c8547aa9f420cccb4a4f9b53920211a2b7f49af146508756e6e38f5cc7e0c4b57f1c8abdd722f65bd3409abd02f
6
+ metadata.gz: 6dcc22eaa684535f9452287e1a486d2de3bc3d38b12be207130e62fb0264ea90087e20c9a9693952b8be3030738a3eebd8681813ccf2f0c2838aa9c33c9ad7b1
7
+ data.tar.gz: 05d9470a2c3426c46b6e64113b728672d841640dea56887d1efc9fcd9c7839b561f09cb2da4d6453bd339222bd07f48885bbae443e55646b90b855cd6bec716f
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.3.0)
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
data/README.md CHANGED
@@ -22,6 +22,11 @@ The information provided by this library should not be construed as an endorseme
22
22
 
23
23
  Past performance is not necessarily indicative of future results. By using this library, you agree that the developers and contributors will not be liable for any losses or damages arising from your use of the library. Use at your own risk.
24
24
 
25
+ ## Possibilties
26
+ While charting and automated trading systems are not part of this library, Quantitative goes a long ways towards giving you powerful tools to build such a system. It is extracted from an automated trading system built in Ruby on Rails and has been used to generate considerable net profits over the years.
27
+
28
+ ![Possibilities](https://github.com/mwlang/quantitative/blob/main/possibilities.png)
29
+
25
30
  ## Installation
26
31
 
27
32
  Install the gem and add to the application's Gemfile by executing:
@@ -1,3 +1,4 @@
1
+
1
2
  module Quant
2
3
  # Dominant Cycles measure the primary cycle within a given range. By default, the library
3
4
  # is wired to look for cycles between 10 and 48 bars. These values can be adjusted by setting
@@ -13,7 +14,11 @@ module Quant
13
14
  # The purpose of these indicators is to compute the dominant cycle and underpin the various
14
15
  # indicators that would otherwise be setting an arbitrary lookback period. This makes the
15
16
  # indicators adaptive and auto-tuning to the market dynamics. Or so the theory goes!
16
- class DominantCycleIndicators < IndicatorsProxy
17
+ class DominantCyclesSource
18
+ def initialize(indicator_source:)
19
+ @indicator_source = indicator_source
20
+ end
21
+
17
22
  # Auto-Correlation Reversals is a method of computing the dominant cycle
18
23
  # by correlating the data stream with itself delayed by a lag.
19
24
  def acr; indicator(Indicators::DominantCycles::Acr) end
@@ -25,11 +30,6 @@ module Quant
25
30
  # and this is the `period` of the dominant cycle.
26
31
  def band_pass; indicator(Indicators::DominantCycles::BandPass) end
27
32
 
28
- # Homodyne means the signal is multiplied by itself. More precisely,
29
- # we want to multiply the signal of the current bar with the complex
30
- # value of the signal one bar ago
31
- def homodyne; indicator(Indicators::DominantCycles::Homodyne) end
32
-
33
33
  # The Dual Differentiator algorithm computes the phase angle from the
34
34
  # analytic signal as the arctangent of the ratio of the imaginary
35
35
  # component to the real component. Further, the angular frequency
@@ -37,13 +37,24 @@ module Quant
37
37
  # derive the cycle period.
38
38
  def differential; indicator(Indicators::DominantCycles::Differential) end
39
39
 
40
+ # Static, arbitrarily set period.
41
+ def half_period; indicator(Indicators::DominantCycles::HalfPeriod) end
42
+
43
+ # Homodyne means the signal is multiplied by itself. More precisely,
44
+ # we want to multiply the signal of the current bar with the complex
45
+ # value of the signal one bar ago
46
+ def homodyne; indicator(Indicators::DominantCycles::Homodyne) end
47
+
40
48
  # The phase accumulation method of computing the dominant cycle measures
41
49
  # the phase at each sample by taking the arctangent of the ratio of the
42
50
  # quadrature component to the in-phase component. The phase is then
43
51
  # accumulated and the period is derived from the phase.
44
52
  def phase_accumulator; indicator(Indicators::DominantCycles::PhaseAccumulator) end
45
53
 
46
- # Static, arbitrarily set period.
47
- def half_period; indicator(Indicators::DominantCycles::HalfPeriod) end
54
+ private
55
+
56
+ def indicator(indicator_class)
57
+ @indicator_source[indicator_class]
58
+ end
48
59
  end
49
60
  end
@@ -1,14 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quant
4
+ # {Quant::Experimental} is an alert emitter for experimental code paths.
5
+ # It will typically be used for new indicators or computations that are not yet
6
+ # fully vetted or tested.
4
7
  module Experimental
5
8
  def self.tracker
6
9
  @tracker ||= {}
7
10
  end
11
+
12
+ def self.rspec_defined?
13
+ defined?("RSpec")
14
+ end
8
15
  end
9
16
 
10
17
  def self.experimental(message)
11
- return if defined?(RSpec)
18
+ return if Experimental.rspec_defined?
12
19
  return if Experimental.tracker[caller.first]
13
20
 
14
21
  Experimental.tracker[caller.first] = message
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "indicator_point"
4
+ require_relative "indicator"
5
+ require_relative "atr"
6
+
7
+ module Quant
8
+ class Indicators
9
+ class AdxPoint < IndicatorPoint
10
+ attribute :dmu, default: 0.0
11
+ attribute :dmd, default: 0.0
12
+ attribute :dmu_ema, default: 0.0
13
+ attribute :dmd_ema, default: 0.0
14
+ attribute :diu, default: 0.0
15
+ attribute :did, default: 0.0
16
+ attribute :di, default: 0.0
17
+ attribute :di_ema, default: 0.0
18
+ attribute :value, default: 0.0
19
+ attribute :inst_stoch, default: 0.0
20
+ attribute :stoch, default: 0.0
21
+ attribute :stoch_up, default: false
22
+ attribute :stoch_turned, default: false
23
+ attribute :ssf, default: 0.0
24
+ attribute :hp, default: 0.0
25
+ end
26
+
27
+ class Adx < Indicator
28
+ depends_on Indicators::Atr
29
+
30
+ def alpha
31
+ bars_to_alpha(dc_period)
32
+ end
33
+
34
+ def scale
35
+ 1.0
36
+ end
37
+
38
+ def period
39
+ dc_period
40
+ end
41
+
42
+ def atr_point
43
+ series.indicators[source].atr.points[t0]
44
+ end
45
+
46
+ def compute
47
+ # To calculate the ADX, first determine the + and - directional movement, or DM.
48
+ # The +DM and -DM are found by calculating the "up-move," or current high minus
49
+ # the previous high, and "down-move," or current low minus the previous low.
50
+ # If the up-move is greater than the down-move and greater than zero, the +DM equals the up-move;
51
+ # otherwise, it equals zero. If the down-move is greater than the up-move and greater than zero,
52
+ # the -DM equals the down-move; otherwise, it equals zero.
53
+ dm_highs = [t0.high_price - t1.high_price, 0.0].max
54
+ dm_lows = [t0.low_price - t1.low_price, 0.0].max
55
+
56
+ p0.dmu = dm_highs > dm_lows ? 0.0 : dm_highs
57
+ p0.dmd = dm_lows > dm_highs ? 0.0 : dm_lows
58
+
59
+ p0.dmu_ema = three_pole_super_smooth :dmu, period:, previous: :dmu_ema
60
+ p0.dmd_ema = three_pole_super_smooth :dmd, period:, previous: :dmd_ema
61
+
62
+ atr_value = atr_point.fast * scale
63
+ return if atr_value == 0.0 || @points.size < period
64
+
65
+ # The positive directional indicator, or +DI, equals 100 times the EMA of +DM divided by the ATR
66
+ # over a given number of time periods. Welles usually used 14 periods.
67
+ # The negative directional indicator, or -DI, equals 100 times the EMA of -DM divided by the ATR.
68
+ p0.diu = (100.0 * p0.dmu_ema) / atr_value
69
+ p0.did = (100.0 * p0.dmd_ema) / atr_value
70
+
71
+ # The ADX indicator itself equals 100 times the EMA of the absolute value of (+DI minus -DI)
72
+ # divided by (+DI plus -DI).
73
+ delta = p0.diu + p0.did
74
+ p0.di = (p0.diu - p1.did).abs / delta
75
+ p0.di_ema = three_pole_super_smooth(:di, period:, previous: :di_ema).clamp(-10.0, 10.0)
76
+
77
+ p0.value = p0.di_ema
78
+ p0.inst_stoch = stochastic :di, period: dc_period
79
+ p0.stoch = three_pole_super_smooth :inst_stoch, period:, previous: :stoch
80
+ end
81
+ end
82
+ end
83
+ 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,71 @@
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
+ attribute :decycle, default: :input
18
+ attribute :hp1, default: 0.0
19
+ attribute :hp2, default: 0.0
20
+ attribute :osc, default: 0.0
21
+ attribute :peak, default: 0.0
22
+ attribute :agc, default: 0.0
23
+ attribute :ift, default: 0.0
24
+ end
25
+
26
+ class Decycler < Indicator
27
+ def max_period
28
+ dc_period
29
+ end
30
+
31
+ def compute_decycler
32
+ alpha = period_to_alpha(max_period)
33
+ p0.decycle = (alpha / 2) * (p0.input + p1.input) + (1.0 - alpha) * p1.decycle
34
+ end
35
+
36
+ # alpha1 = (Cosine(.707*360 / HPPeriod1) + Sine (.707*360 / HPPeriod1) - 1) / Cosine(.707*360 / HPPeriod1);
37
+ # HP1 = (1 - alpha1 / 2)*(1 - alpha1 / 2)*(Close - 2*Close[1] + Close[2]) + 2*(1 - alpha1)*HP1[1] - (1 - alpha1)*(1 - alpha1)*HP1[2];
38
+ def compute_hp(period, hp)
39
+ radians = deg2rad(360)
40
+ c = Math.cos(0.707 * radians / period)
41
+ s = Math.sin(0.707 * radians / period)
42
+ alpha = (c + s - 1) / c
43
+ (1 - alpha / 2)**2 * (p0.input - 2 * p1.input + p2.input) + 2 * (1 - alpha) * p1.send(hp) - (1 - alpha) * (1 - alpha) * p2.send(hp)
44
+ end
45
+
46
+ def compute_oscillator
47
+ p0.hp1 = compute_hp(min_period, :hp1)
48
+ p0.hp2 = compute_hp(max_period, :hp2)
49
+ p0.osc = p0.hp2 - p0.hp1
50
+ end
51
+
52
+ # AGC is constrained to -1.0 to 1.0
53
+ # The peak decays at a rate of 0.991 per bar
54
+ def compute_automatic_gain_control
55
+ p0.peak = [p0.osc.abs, 0.991 * p1.peak].max
56
+ p0.agc = p0.peak.zero? ? p0.osc : p0.osc / p0.peak
57
+ end
58
+
59
+ def compute_inverse_fisher_transform
60
+ p0.ift = inverse_fisher_transform(p0.agc, scale_factor: 5.0)
61
+ end
62
+
63
+ def compute
64
+ compute_decycler
65
+ compute_oscillator
66
+ compute_automatic_gain_control
67
+ compute_inverse_fisher_transform
68
+ end
69
+ end
70
+ end
71
+ 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
@@ -42,6 +44,15 @@ module Quant
42
44
  end
43
45
 
44
46
  class DominantCycle < Indicators::Indicator
47
+ def priority
48
+ DOMINANT_CYCLES_PRIORITY
49
+ end
50
+
51
+ # Dominant Cycle Indicators should not themselves have a dominant cycle indicator
52
+ def dominant_cycle_indicator_class
53
+ nil
54
+ end
55
+
45
56
  def points_class
46
57
  Object.const_get "Quant::Indicators::DominantCycles::#{indicator_name}Point"
47
58
  rescue NameError
@@ -76,11 +87,6 @@ module Quant
76
87
  [p0.period.to_i, min_period].max
77
88
  end
78
89
 
79
- def period_points(max_period)
80
- extent = [values.size, max_period].min
81
- values[-extent, extent]
82
- end
83
-
84
90
  def compute
85
91
  compute_input_data_points
86
92
  compute_quadrature_components
@@ -141,4 +147,4 @@ module Quant
141
147
  end
142
148
  end
143
149
  end
144
- end
150
+ 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,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ class Indicators
5
+ class FramaPoint < IndicatorPoint
6
+ attribute :frama, default: :input
7
+ attribute :dimension, default: 0.0
8
+ attribute :alpha, default: 0.0
9
+ end
10
+
11
+ # FRAMA (FRactal Adaptive Moving Average). A nonlinear moving average
12
+ # is derived using the Hurst exponent. It rapidly follows significant
13
+ # changes in price but becomes very flat in congestion zones so that
14
+ # bad whipsaw trades can be eliminated.
15
+ #
16
+ # SOURCE: http://www.mesasoftware.com/papers/FRAMA.pdf
17
+ class Frama < Indicator
18
+ using Quant
19
+
20
+ # The max_period is divided into two smaller, equal periods, so must be even
21
+ def max_period
22
+ @max_period ||= begin
23
+ mp = super
24
+ mp.even? ? mp : mp + 1
25
+ end
26
+ end
27
+
28
+ def half_period
29
+ max_period / 2
30
+ end
31
+
32
+ def compute
33
+ pp = period_points(max_period).map(&:input)
34
+ return if pp.size < max_period
35
+
36
+ n3 = (pp.maximum - pp.minimum) / max_period
37
+
38
+ ppn2 = pp.first(half_period)
39
+ n2 = (ppn2.maximum - ppn2.minimum) / half_period
40
+
41
+ ppn1 = pp.last(half_period)
42
+ n1 = (ppn1.maximum - ppn1.minimum) / half_period
43
+
44
+ p0.dimension = (Math.log(n1 + n2) - Math.log(n3)) / Math.log(2)
45
+ p0.alpha = Math.exp(-4.6 * (p0.dimension - 1.0)).clamp(0.01, 1.0)
46
+ p0.frama = (p0.alpha * p0.input) + ((1 - p0.alpha) * p1.frama)
47
+ end
48
+ end
49
+ end
50
+ end