quantitative 0.2.2 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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