quantitative 0.2.2 → 0.3.1

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +3 -1
  3. data/README.md +5 -0
  4. data/lib/quant/asset.rb +0 -2
  5. data/lib/quant/{dominant_cycle_indicators.rb → dominant_cycles_source.rb} +18 -10
  6. data/lib/quant/experimental.rb +11 -2
  7. data/lib/quant/indicators/adx.rb +3 -4
  8. data/lib/quant/indicators/atr.rb +1 -4
  9. data/lib/quant/indicators/cci.rb +1 -1
  10. data/lib/quant/indicators/decycler.rb +17 -31
  11. data/lib/quant/indicators/dominant_cycles/acr.rb +3 -6
  12. data/lib/quant/indicators/dominant_cycles/band_pass.rb +2 -4
  13. data/lib/quant/indicators/dominant_cycles/differential.rb +2 -2
  14. data/lib/quant/indicators/dominant_cycles/dominant_cycle.rb +11 -4
  15. data/lib/quant/indicators/dominant_cycles/half_period.rb +2 -4
  16. data/lib/quant/indicators/dominant_cycles/homodyne.rb +2 -5
  17. data/lib/quant/indicators/dominant_cycles/phase_accumulator.rb +2 -4
  18. data/lib/quant/indicators/frama.rb +6 -9
  19. data/lib/quant/indicators/indicator.rb +46 -3
  20. data/lib/quant/indicators/indicator_point.rb +1 -1
  21. data/lib/quant/indicators/mama.rb +1 -1
  22. data/lib/quant/indicators/mesa.rb +1 -1
  23. data/lib/quant/indicators/ping.rb +1 -1
  24. data/lib/quant/indicators/pivot.rb +107 -0
  25. data/lib/quant/indicators/pivots/atr.rb +41 -0
  26. data/lib/quant/indicators/pivots/bollinger.rb +45 -0
  27. data/lib/quant/indicators/pivots/camarilla.rb +61 -0
  28. data/lib/quant/indicators/pivots/classic.rb +24 -0
  29. data/lib/quant/indicators/pivots/demark.rb +50 -0
  30. data/lib/quant/indicators/pivots/donchian.rb +40 -0
  31. data/lib/quant/indicators/pivots/fibbonacci.rb +22 -0
  32. data/lib/quant/indicators/pivots/guppy.rb +39 -0
  33. data/lib/quant/indicators/pivots/keltner.rb +43 -0
  34. data/lib/quant/indicators/pivots/murrey.rb +33 -0
  35. data/lib/quant/indicators/pivots/traditional.rb +36 -0
  36. data/lib/quant/indicators/pivots/woodie.rb +59 -0
  37. data/lib/quant/indicators.rb +1 -13
  38. data/lib/quant/indicators_source.rb +139 -0
  39. data/lib/quant/indicators_sources.rb +36 -10
  40. data/lib/quant/mixins/filters.rb +0 -3
  41. data/lib/quant/mixins/moving_averages.rb +0 -3
  42. data/lib/quant/pivots_source.rb +28 -0
  43. data/lib/quant/refinements/array.rb +14 -0
  44. data/lib/quant/series.rb +8 -19
  45. data/lib/quant/settings/indicators.rb +11 -0
  46. data/lib/quant/ticks/ohlc.rb +0 -2
  47. data/lib/quant/ticks/spot.rb +0 -2
  48. data/lib/quant/version.rb +1 -1
  49. data/lib/quantitative.rb +21 -4
  50. data/possibilities.png +0 -0
  51. metadata +34 -5
  52. data/lib/quant/indicators_proxy.rb +0 -68
@@ -0,0 +1,41 @@
1
+ module Quant
2
+ module Indicators
3
+ module Pivots
4
+ class Atr < Pivot
5
+ depends_on Indicators::Atr
6
+
7
+ def atr_point
8
+ series.indicators[source].atr.points[t0]
9
+ end
10
+
11
+ def scale
12
+ 5.0
13
+ end
14
+
15
+ def atr_value
16
+ atr_point.slow * scale
17
+ end
18
+
19
+ def compute_midpoint
20
+ p0.midpoint = two_pole_super_smooth :input, previous: :midpoint, period: averaging_period
21
+ end
22
+
23
+ def compute_bands
24
+ p0.h6 = p0.midpoint + 1.000 * atr_value
25
+ p0.h5 = p0.midpoint + 0.786 * atr_value
26
+ p0.h4 = p0.midpoint + 0.618 * atr_value
27
+ p0.h3 = p0.midpoint + 0.500 * atr_value
28
+ p0.h2 = p0.midpoint + 0.382 * atr_value
29
+ p0.h1 = p0.midpoint + 0.236 * atr_value
30
+
31
+ p0.l1 = p0.midpoint - 0.236 * atr_value
32
+ p0.l2 = p0.midpoint - 0.382 * atr_value
33
+ p0.l3 = p0.midpoint - 0.500 * atr_value
34
+ p0.l4 = p0.midpoint - 0.618 * atr_value
35
+ p0.l5 = p0.midpoint - 0.786 * atr_value
36
+ p0.l6 = p0.midpoint - 1.000 * atr_value
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Indicators
5
+ module Pivots
6
+ class Bollinger < Pivot
7
+ using Quant
8
+
9
+ def compute_midpoint
10
+ values = period_points(half_period).map(&:input)
11
+ alpha = bars_to_alpha(half_period)
12
+
13
+ p0.midpoint = alpha * values.mean + (1 - alpha) * p1.midpoint
14
+ p0.std_dev = values.standard_deviation(p0.midpoint)
15
+ end
16
+
17
+ def compute_bands
18
+ p0.h1 = p0.midpoint + p0.std_dev * 1.0
19
+ p0.l1 = p0.midpoint - p0.std_dev * 1.0
20
+
21
+ p0.h2 = p0.midpoint + p0.std_dev * 1.5
22
+ p0.l2 = p0.midpoint - p0.std_dev * 1.5
23
+
24
+ p0.h3 = p0.midpoint + p0.std_dev * 1.75
25
+ p0.l3 = p0.midpoint - p0.std_dev * 1.75
26
+
27
+ p0.h4 = p0.midpoint + p0.std_dev * 2.0
28
+ p0.l4 = p0.midpoint - p0.std_dev * 2.0
29
+
30
+ p0.h5 = p0.midpoint + p0.std_dev * 2.25
31
+ p0.l5 = p0.midpoint - p0.std_dev * 2.25
32
+
33
+ p0.h6 = p0.midpoint + p0.std_dev * 2.5
34
+ p0.l6 = p0.midpoint - p0.std_dev * 2.5
35
+
36
+ p0.h7 = p0.midpoint + p0.std_dev * 2.75
37
+ p0.l7 = p0.midpoint - p0.std_dev * 2.75
38
+
39
+ p0.h8 = p0.midpoint + p0.std_dev * 3.0
40
+ p0.l8 = p0.midpoint - p0.std_dev * 3.0
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Indicators
5
+ module Pivots
6
+ # Camarilla pivot point calculations are rather straightforward. We need to
7
+ # input the previous day’s open, high, low and close. The formulas for each
8
+ # resistance and support level are:
9
+ #
10
+ # R4 = Close + (High – Low) * 1.1/2
11
+ # R3 = Close + (High – Low) * 1.1/4
12
+ # R2 = Close + (High – Low) * 1.1/6
13
+ # R1 = Close + (High – Low) * 1.1/12
14
+ # S1 = Close – (High – Low) * 1.1/12
15
+ # S2 = Close – (High – Low) * 1.1/6
16
+ # S3 = Close – (High – Low) * 1.1/4
17
+ # S4 = Close – (High – Low) * 1.1/2
18
+ #
19
+ # The calculation for further resistance and support levels varies from this
20
+ # norm. These levels can come into play during strong trend moves, so it’s
21
+ # important to understand how to identify them. For example, R5, R6, S5 and S6
22
+ # are calculated as follows:
23
+ #
24
+ # R5 = R4 + 1.168 * (R4 – R3)
25
+ # R6 = (High/Low) * Close
26
+ #
27
+ # S5 = S4 – 1.168 * (S3 – S4)
28
+ # S6 = Close – (R6 – Close)
29
+ class Camarilla < Pivot
30
+ def multiplier
31
+ 1.1
32
+ end
33
+
34
+ def compute_midpoint
35
+ p0.midpoint = t0.close_price
36
+ end
37
+
38
+ def compute_bands
39
+ mp_plus_range = p0.midpoint + p0.range
40
+ mp_minus_range = p0.midpoint - p0.range
41
+
42
+ p0.h4 = mp_plus_range * (1.1 / 2.0)
43
+ p0.h3 = mp_plus_range * (1.1 / 4.0)
44
+ p0.h2 = mp_plus_range * (1.1 / 6.0)
45
+ p0.h1 = mp_plus_range * (1.1 / 12.0)
46
+
47
+ p0.l1 = mp_minus_range * (1.1 / 12.0)
48
+ p0.l2 = mp_minus_range * (1.1 / 6.0)
49
+ p0.l3 = mp_minus_range * (1.1 / 4.0)
50
+ p0.l4 = mp_minus_range * (1.1 / 2.0)
51
+
52
+ p0.h5 = p0.h4 + 1.168 * (p0.h4 - p0.h3)
53
+ p0.h6 = p0.midpoint * (p0.high_price / p0.low_price)
54
+
55
+ p0.l5 = p0.l4 - 1.168 * (p0.l3 - p0.l4)
56
+ p0.l6 = p0.midpoint - (p0.h6 - p0.midpoint)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Indicators
5
+ module Pivots
6
+ class Classic < Pivot
7
+ def compute_midpoint
8
+ p0.midpoint = super_smoother :input, previous: :midpoint, period: averaging_period
9
+ end
10
+
11
+ def compute_bands
12
+ p0.h1 = p0.midpoint * 2.0 - p0.avg_low
13
+ p0.l1 = p0.midpoint * 2.0 - p0.avg_high
14
+
15
+ p0.h2 = p0.midpoint + p0.avg_range
16
+ p0.l2 = p0.midpoint - p0.avg_range
17
+
18
+ p0.h3 = p0.midpoint + 2.0 * p0.avg_range
19
+ p0.l3 = p0.midpoint - 2.0 * p0.avg_range
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Indicators
5
+ module Pivots
6
+ # The value of X in the formula below depends on where the Close of the market is.
7
+ # If Close = Open then X = (H + L + (C * 2))
8
+
9
+ # If Close > Open then X = ((H * 2) + L + C)
10
+
11
+ # If Close < Open then X = (H + (L * 2) + C)
12
+
13
+ # R1 = X / 2 - L
14
+ # PP = X / 4 (this is not an official DeMark number but merely a reference point based on the calculation of X)
15
+ # S1 = X / 2 - H
16
+ class Demark < Pivot
17
+ def averaging_period
18
+ min_period / 2
19
+ end
20
+
21
+ def x_factor
22
+ if t0.close_price == t0.open_price
23
+ ((2.0 * t0.close_price) + p0.avg_high + p0.avg_low)
24
+ elsif t0.close_price > t0.open_price
25
+ ((2.0 * p0.avg_high) + p0.avg_low + t0.close_price)
26
+ else
27
+ ((2.0 * p0.avg_low) + p0.avg_high + t0.close_price)
28
+ end
29
+ end
30
+
31
+ def compute_value
32
+ p0.input = x_factor
33
+ end
34
+
35
+ def compute_midpoint
36
+ p0.midpoint = p0.input / 4.0
37
+ p0.midpoint = super_smoother :midpoint, previous: :midpoint, period: averaging_period
38
+ end
39
+
40
+ def compute_bands
41
+ p0.h1 = (p0.input / 2.0) - p0.avg_high
42
+ p0.h1 = super_smoother :h1, previous: :h1, period: averaging_period
43
+
44
+ p0.l1 = (p0.input / 2.0) - p0.avg_low
45
+ p0.l1 = super_smoother :l1, previous: :l1, period: averaging_period
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Indicators
5
+ module Pivots
6
+ class Donchian < Pivot
7
+ using Quant
8
+
9
+ def st_period; min_period end
10
+ def mt_period; half_period end
11
+ def lt_period; max_period end
12
+
13
+ def st_highs; @st_highs ||= [].max_size!(st_period) end
14
+ def st_lows; @st_lows ||= [].max_size!(st_period) end
15
+ def mt_highs; @mt_highs ||= [].max_size!(mt_period) end
16
+ def mt_lows; @mt_lows ||= [].max_size!(mt_period) end
17
+ def lt_highs; @lt_highs ||= [].max_size!(lt_period) end
18
+ def lt_lows; @lt_lows ||= [].max_size!(lt_period) end
19
+
20
+ def compute_bands
21
+ st_highs << p0.high_price
22
+ st_lows << p0.low_price
23
+ mt_highs << p0.high_price
24
+ mt_lows << p0.low_price
25
+ lt_highs << p0.high_price
26
+ lt_lows << p0.low_price
27
+
28
+ p0.h1 = @st_highs.maximum
29
+ p0.l1 = @st_lows.minimum
30
+
31
+ p0.h2 = @mt_highs.maximum
32
+ p0.l2 = @mt_lows.minimum
33
+
34
+ p0.h3 = @lt_highs.maximum
35
+ p0.l3 = @lt_lows.minimum
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,22 @@
1
+ module Quant
2
+ module Indicators
3
+ module Pivots
4
+ class Fibbonacci < Pivot
5
+ def averaging_period
6
+ half_period
7
+ end
8
+
9
+ def fibbonacci_series
10
+ [0.146, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0, 1.146]
11
+ end
12
+
13
+ def compute_bands
14
+ fibbonacci_series.each_with_index do |ratio, index|
15
+ p0[index + 1] = p0.midpoint + ratio * p0.avg_range
16
+ p0[-index - 1] = p0.midpoint - ratio * p0.avg_range
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,39 @@
1
+ module Quant
2
+ module Indicators
3
+ module Pivots
4
+ class Guppy < Pivot
5
+ def guppy_ema(period, band)
6
+ return p0.input unless p1[band]
7
+
8
+ alpha = bars_to_alpha(period)
9
+ alpha * p0.input + (1 - alpha) * p1[band]
10
+ end
11
+
12
+ def compute_midpoint
13
+ p0.midpoint = guppy_ema(3, 0)
14
+ end
15
+
16
+ # The short-term MAs are typically set at 3, 5, 8, 10, 12, and 15 periods. The
17
+ # longer-term MAs are typically set at 30, 35, 40, 45, 50, and 60.
18
+ def compute
19
+ p0[1] = guppy_ema(5, 1)
20
+ p0[2] = guppy_ema(8, 2)
21
+ p0[3] = guppy_ema(10, 3)
22
+ p0[4] = guppy_ema(12, 4)
23
+ p0[5] = guppy_ema(15, 5)
24
+ p0[6] = guppy_ema(20, 6)
25
+ p0[7] = guppy_ema(25, 7)
26
+
27
+ p0[-1] = guppy_ema(30, -1)
28
+ p0[-2] = guppy_ema(35, -2)
29
+ p0[-3] = guppy_ema(40, -3)
30
+ p0[-4] = guppy_ema(45, -4)
31
+ p0[-5] = guppy_ema(50, -5)
32
+ p0[-6] = guppy_ema(60, -6)
33
+ p0[-7] = guppy_ema(120, -7)
34
+ p0[-8] = guppy_ema(200, -8)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,43 @@
1
+ module Quant
2
+ module Indicators
3
+ module Pivots
4
+ class Keltner < Pivot
5
+ depends_on Indicators::Atr
6
+
7
+ def atr_point
8
+ series.indicators[source].atr.points[t0]
9
+ end
10
+
11
+ def scale
12
+ 5.0
13
+ end
14
+
15
+ def alpha
16
+ bars_to_alpha(min_period)
17
+ end
18
+
19
+ def compute_midpoint
20
+ p0.midpoint = alpha * p0.input + (1 - alpha) * p1.midpoint
21
+ end
22
+
23
+ def compute_bands
24
+ atr_value = atr_point.slow * scale
25
+
26
+ p0.h6 = p0.midpoint + 1.000 * atr_value
27
+ p0.h5 = p0.midpoint + 0.786 * atr_value
28
+ p0.h4 = p0.midpoint + 0.618 * atr_value
29
+ p0.h3 = p0.midpoint + 0.500 * atr_value
30
+ p0.h2 = p0.midpoint + 0.382 * atr_value
31
+ p0.h1 = p0.midpoint + 0.236 * atr_value
32
+
33
+ p0.l1 = p0.midpoint - 0.236 * atr_value
34
+ p0.l2 = p0.midpoint - 0.382 * atr_value
35
+ p0.l3 = p0.midpoint - 0.500 * atr_value
36
+ p0.l4 = p0.midpoint - 0.618 * atr_value
37
+ p0.l5 = p0.midpoint - 0.786 * atr_value
38
+ p0.l6 = p0.midpoint - 1.000 * atr_value
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,33 @@
1
+
2
+ module Quant
3
+ module Indicators
4
+ module Pivots
5
+ class Murrey < Pivot
6
+ def multiplier
7
+ 0.125
8
+ end
9
+
10
+ def compute_midpoint
11
+ p0.input = (p0.highest - p0.lowest) * multiplier
12
+ p0.midpoint = p0.lowest + (p0.input * 4.0)
13
+ end
14
+
15
+ def compute_bands
16
+ p0.h6 = p0.midpoint + p0.input * 6.0
17
+ p0.h5 = p0.midpoint + p0.input * 5.0
18
+ p0.h4 = p0.midpoint + p0.input * 4.0
19
+ p0.h3 = p0.midpoint + p0.input * 3.0
20
+ p0.h2 = p0.midpoint + p0.input * 2.0
21
+ p0.h1 = p0.midpoint + p0.input * 1.0
22
+
23
+ p0.l1 = p0.midpoint - p0.input * 1.0
24
+ p0.l2 = p0.midpoint - p0.input * 2.0
25
+ p0.l3 = p0.midpoint - p0.input * 3.0
26
+ p0.l4 = p0.midpoint - p0.input * 4.0
27
+ p0.l5 = p0.midpoint - p0.input * 5.0
28
+ p0.l6 = p0.midpoint - p0.input * 6.0
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ module Quant
2
+ module Indicators
3
+ module Pivots
4
+ class Traditional < Pivot
5
+ def multiplier
6
+ 2.0
7
+ end
8
+
9
+ # Pivot Point (PP) = (High + Low + Close) / 3
10
+ def compute_midpoint
11
+ p0.midpoint = p0.input
12
+ end
13
+
14
+ def compute_bands
15
+ # Third Resistance (R3) = High + 2 × (PP - Low)
16
+ p0.h3 = p0.high_price + (multiplier * (p0.midpoint - p0.low_price))
17
+
18
+ # Second Resistance (R2) = PP + (High - Low)
19
+ p0.h2 = p0.midpoint + p0.range
20
+
21
+ # First Resistance (R1) = (2 × PP) - Low
22
+ p0.h1 = p0.midpoint * multiplier - p0.low_price
23
+
24
+ # First Support (S1) = (2 × PP) - High
25
+ p0.l1 = p0.midpoint * multiplier - p0.high_price
26
+
27
+ # Second Support (S2) = PP - (High - Low)
28
+ p0.l2 = p0.midpoint - p0.range
29
+
30
+ # Third Support (S3) = Low - 2 × (High - PP)
31
+ p0.l3 = p0.low_price - (multiplier * (p0.high_price - p0.midpoint))
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Indicators
5
+ module Pivots
6
+ # One of the key differences in calculating Woodie's Pivot Point to other pivot
7
+ # points is that the current session's open price is used in the PP formula with
8
+ # the previous session's high and low. At the time-of-day that we calculate the
9
+ # pivot points on this site in our Daily Notes we do not have the opening price
10
+ # so we use the Classic formula for the Pivot Point and vary the R3 and R4
11
+ # formula as per Woodie's formulas.
12
+
13
+ # Formulas:
14
+ # R4 = R3 + RANGE
15
+ # R3 = H + 2 * (PP - L) (same as: R1 + RANGE)
16
+ # R2 = PP + RANGE
17
+ # R1 = (2 * PP) - LOW
18
+
19
+ # PP = (HIGH + LOW + (TODAY'S OPEN * 2)) / 4
20
+ # S1 = (2 * PP) - HIGH
21
+ # S2 = PP - RANGE
22
+ # S3 = L - 2 * (H - PP) (same as: S1 - RANGE)
23
+ # S4 = S3 - RANGE
24
+ class Woodie < Pivot
25
+ def compute_value
26
+ p0.input = (t1.high_price + t1.low_price + 2.0 * t0.open_price) / 4.0
27
+ end
28
+
29
+ def compute_bands
30
+ Quant.experimental("Woodie appears erratic, is unproven and may be incorrect.")
31
+
32
+ # R1 = (2 * PP) - LOW
33
+ p0.h1 = 2.0 * p0.midpoint - t1.low_price
34
+
35
+ # R2 = PP + RANGE
36
+ p0.h2 = p0.midpoint + p0.range
37
+
38
+ # R3 = H + 2 * (PP - L) (same as: R1 + RANGE)
39
+ p0.h3 = t1.high_price + 2.0 * (p0.midpoint - t1.low_price)
40
+
41
+ # R4 = R3 + RANGE
42
+ p0.h4 = p0.h3 + p0.range
43
+
44
+ # S1 = (2 * PP) - HIGH
45
+ p0.l1 = 2.0 * p0.midpoint - t1.high_price
46
+
47
+ # S2 = PP - RANGE
48
+ p0.l2 = p0.midpoint - p0.range
49
+
50
+ # S3 = L - 2 * (H - PP) (same as: S1 - RANGE)
51
+ p0.l3 = t1.low_price - 2.0 * (t1.high_price - p0.midpoint)
52
+
53
+ # S4 = S3 - RANGE
54
+ p0.l4 = p0.l3 - p0.range
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,18 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "indicators_proxy"
4
3
  module Quant
5
- # TODO: build an Indicator registry so new indicators can be added and
6
- # used outside those shipped with the library.
7
- class Indicators < IndicatorsProxy
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
13
-
14
- def dominant_cycles
15
- @dominant_cycles ||= Quant::DominantCycleIndicators.new(series:, source:)
16
- end
4
+ module Indicators
17
5
  end
18
6
  end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ # {Quant::IndicatorSource} holds a collection of {Quant::Indicators::Indicator} for a given input source.
5
+ # This class ensures dominant cycle computations come before other indicators that depend on them.
6
+ #
7
+ # The {Quant::IndicatorSource} class is responsible for lazily loading indicators
8
+ # so that not all indicators are always engaged and computing their values.
9
+ # If the indicator is never accessed, it's never computed, saving valuable
10
+ # processing CPU cycles.
11
+ #
12
+ # Indicators are generally built around the concept of a source input value and
13
+ # that source is designated by the source parameter when instantiating the
14
+ # {Quant::IndicatorSource} class.
15
+ #
16
+ # By design, the {Quant::Indicators::Indicator} class holds the {Quant::Ticks::Tick} instance
17
+ # alongside the indicator's computed values for that tick.
18
+ class IndicatorsSource
19
+ attr_reader :series, :source, :dominant_cycles, :pivots
20
+
21
+ def initialize(series:, source:)
22
+ @series = series
23
+ @source = source
24
+ @indicators = {}
25
+ @ordered_indicators = []
26
+ @dominant_cycles = DominantCyclesSource.new(indicator_source: self)
27
+ @pivots = PivotsSource.new(indicator_source: self)
28
+ end
29
+
30
+ def [](indicator_class)
31
+ indicator(indicator_class)
32
+ end
33
+
34
+ def <<(tick)
35
+ @ordered_indicators.each { |indicator| indicator << tick }
36
+ end
37
+
38
+ def adx; indicator(Indicators::Adx) end
39
+ def atr; indicator(Indicators::Atr) end
40
+ def cci; indicator(Indicators::Cci) end
41
+ def decycler; indicator(Indicators::Decycler) end
42
+ def frama; indicator(Indicators::Frama) end
43
+ def mama; indicator(Indicators::Mama) end
44
+ def mesa; indicator(Indicators::Mesa) end
45
+ def ping; indicator(Indicators::Ping) end
46
+
47
+ # Attaches a given Indicator class and defines the method for
48
+ # accessing it using the given name. Indicators take care of
49
+ # computing their values when first attached to a populated
50
+ # series.
51
+ #
52
+ # The indicators shipped with the library are all wired into the framework, thus
53
+ # this method should be used for custom indicators not shipped with the library.
54
+ #
55
+ # @param name [Symbol] The name of the method to define for accessing the indicator.
56
+ # @param indicator_class [Class] The class of the indicator to attach.
57
+ # @example
58
+ # series.indicators.oc2.attach(name: :foo, indicator_class: Indicators::Foo)
59
+ def attach(name:, indicator_class:)
60
+ define_singleton_method(name) { indicator(indicator_class) }
61
+ end
62
+
63
+ def dominant_cycle
64
+ indicator(dominant_cycle_indicator_class)
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :indicators, :ordered_indicators
70
+
71
+ def dominant_cycle_indicator_class
72
+ Quant.config.indicators.dominant_cycle_indicator_class
73
+ end
74
+
75
+ # Instantiates the indicator class and stores it in the indicators hash. Once
76
+ # prepared, the indicator becomes active and all ticks pushed into the series
77
+ # are sent to the indicator for processing.
78
+ def indicator(indicator_class)
79
+ indicators[indicator_class] ||= new_indicator(indicator_class)
80
+ end
81
+
82
+ # Instantiates a new indicator and adds it to the collection of indicators.
83
+ # This method is responsible for adding dependent indicators and the dominant cycle
84
+ # indicator.
85
+ def new_indicator(indicator_class)
86
+ indicator_class.new(series:, source:).tap do |indicator|
87
+ add_dominant_cycle_indicator(indicator.dominant_cycle_indicator_class, indicator)
88
+ add_dependent_indicators(indicator_class.dependent_indicator_classes, indicator)
89
+ add_indicator(indicator_class, indicator)
90
+ end
91
+ end
92
+
93
+ # Adds a new indicator to the collection of indicators. Once added, every
94
+ # tick added to the series triggers the indicator's compute to fire.
95
+ # The ordered indicators list is adjusted after adding the new indicator.
96
+ def add_indicator(indicator_class, new_indicator)
97
+ return if indicators[indicator_class]
98
+
99
+ indicators[indicator_class] = new_indicator
100
+ @ordered_indicators = (ordered_indicators << new_indicator).sort_by(&:priority)
101
+ new_indicator
102
+ end
103
+
104
+ # Adds dependent indicators to the indicator collection. This method is reentrant and
105
+ # will also add depencies of the dependent indicators.
106
+ # Dependent indicators automatically adjust priority based on the dependency.
107
+ def add_dependent_indicators(indicator_classes, indicator)
108
+ return if indicator_classes.empty?
109
+
110
+ # Dependent indicators should come after dominant cycle indicators, but before the
111
+ # indicators that depend on them.
112
+ dependency_priority = (Quant::Indicators::Indicator::DEPENDENCY_PRIORITY + indicator.priority) / 2
113
+
114
+ indicator_classes.each_with_index do |indicator_class, index|
115
+ next if indicators[indicator_class]
116
+
117
+ new_indicator = indicator_class.new(series:, source:)
118
+ new_indicator.define_singleton_method(:priority) { dependency_priority + index }
119
+ add_dependent_indicators(indicator_class.dependent_indicator_classes, new_indicator)
120
+ add_indicator(indicator_class, new_indicator)
121
+ end
122
+ end
123
+
124
+ # Adds the dominant cycle indicator to the collection of indicators. Indicators added
125
+ # by this method must be a subclass of {Quant::Indicators::DominantCycles::DominantCycle}.
126
+ def add_dominant_cycle_indicator(dominant_cycle_class, indicator)
127
+ return if indicator.is_a?(Indicators::DominantCycles::DominantCycle)
128
+ return unless dominant_cycle_class
129
+ return if indicators[dominant_cycle_class]
130
+
131
+ dominant_cycle = dominant_cycle_class.new(series:, source:)
132
+ add_indicator(dominant_cycle_class, dominant_cycle)
133
+ end
134
+
135
+ def invalid_source_error(source:)
136
+ raise InvalidIndicatorSource, "Invalid indicator source: #{source.inspect}"
137
+ end
138
+ end
139
+ end