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
@@ -0,0 +1,22 @@
1
+ module Quant
2
+ class Indicators
3
+ class 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
+ class Indicators
3
+ class 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
+ class Indicators
3
+ class 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,34 @@
1
+ require_relative '../pivot'
2
+
3
+ module Quant
4
+ class Indicators
5
+ class Pivots
6
+ class Murrey < Pivot
7
+ def multiplier
8
+ 0.125
9
+ end
10
+
11
+ def compute_midpoint
12
+ p0.input = (p0.highest - p0.lowest) * multiplier
13
+ p0.midpoint = p0.lowest + (p0.input * 4.0)
14
+ end
15
+
16
+ def compute_bands
17
+ p0.h6 = p0.midpoint + p0.input * 6.0
18
+ p0.h5 = p0.midpoint + p0.input * 5.0
19
+ p0.h4 = p0.midpoint + p0.input * 4.0
20
+ p0.h3 = p0.midpoint + p0.input * 3.0
21
+ p0.h2 = p0.midpoint + p0.input * 2.0
22
+ p0.h1 = p0.midpoint + p0.input * 1.0
23
+
24
+ p0.l1 = p0.midpoint - p0.input * 1.0
25
+ p0.l2 = p0.midpoint - p0.input * 2.0
26
+ p0.l3 = p0.midpoint - p0.input * 3.0
27
+ p0.l4 = p0.midpoint - p0.input * 4.0
28
+ p0.l5 = p0.midpoint - p0.input * 5.0
29
+ p0.l6 = p0.midpoint - p0.input * 6.0
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,36 @@
1
+ module Quant
2
+ class Indicators
3
+ class 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
+ require_relative '../pivot'
2
+
3
+ module Quant
4
+ class Indicators
5
+ class 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
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dominant_cycles_source"
4
+ module Quant
5
+ # {Quant::IndicatorSource} holds a collection of {Quant::Indicators::Indicator} for a given input source.
6
+ # This class ensures dominant cycle computations come before other indicators that depend on them.
7
+ #
8
+ # The {Quant::IndicatorSource} class is responsible for lazily loading indicators
9
+ # so that not all indicators are always engaged and computing their values.
10
+ # If the indicator is never accessed, it's never computed, saving valuable
11
+ # processing CPU cycles.
12
+ #
13
+ # Indicators are generally built around the concept of a source input value and
14
+ # that source is designated by the source parameter when instantiating the
15
+ # {Quant::IndicatorSource} class.
16
+ #
17
+ # By design, the {Quant::Indicators::Indicator} class holds the {Quant::Ticks::Tick} instance
18
+ # alongside the indicator's computed values for that tick.
19
+ class IndicatorsSource
20
+ attr_reader :series, :source, :dominant_cycles, :pivots
21
+
22
+ def initialize(series:, source:)
23
+ @series = series
24
+ @source = source
25
+ @indicators = {}
26
+ @ordered_indicators = []
27
+ @dominant_cycles = DominantCyclesSource.new(indicator_source: self)
28
+ @pivots = PivotsSource.new(indicator_source: self)
29
+ end
30
+
31
+ def [](indicator_class)
32
+ indicator(indicator_class)
33
+ end
34
+
35
+ def <<(tick)
36
+ @ordered_indicators.each { |indicator| indicator << tick }
37
+ end
38
+
39
+ def adx; indicator(Indicators::Adx) end
40
+ def atr; indicator(Indicators::Atr) end
41
+ def cci; indicator(Indicators::Cci) end
42
+ def decycler; indicator(Indicators::Decycler) end
43
+ def frama; indicator(Indicators::Frama) end
44
+ def mama; indicator(Indicators::Mama) end
45
+ def mesa; indicator(Indicators::Mesa) end
46
+ def ping; indicator(Indicators::Ping) end
47
+
48
+ # Attaches a given Indicator class and defines the method for
49
+ # accessing it using the given name. Indicators take care of
50
+ # computing their values when first attached to a populated
51
+ # series.
52
+ #
53
+ # The indicators shipped with the library are all wired into the framework, thus
54
+ # this method should be used for custom indicators not shipped with the library.
55
+ #
56
+ # @param name [Symbol] The name of the method to define for accessing the indicator.
57
+ # @param indicator_class [Class] The class of the indicator to attach.
58
+ # @example
59
+ # series.indicators.oc2.attach(name: :foo, indicator_class: Indicators::Foo)
60
+ def attach(name:, indicator_class:)
61
+ define_singleton_method(name) { indicator(indicator_class) }
62
+ end
63
+
64
+ def dominant_cycle
65
+ indicator(dominant_cycle_indicator_class)
66
+ end
67
+
68
+ private
69
+
70
+ attr_reader :indicators, :ordered_indicators
71
+
72
+ def dominant_cycle_indicator_class
73
+ Quant.config.indicators.dominant_cycle_indicator_class
74
+ end
75
+
76
+ # Instantiates the indicator class and stores it in the indicators hash. Once
77
+ # prepared, the indicator becomes active and all ticks pushed into the series
78
+ # are sent to the indicator for processing.
79
+ def indicator(indicator_class)
80
+ indicators[indicator_class] ||= new_indicator(indicator_class)
81
+ end
82
+
83
+ # Instantiates a new indicator and adds it to the collection of indicators.
84
+ # This method is responsible for adding dependent indicators and the dominant cycle
85
+ # indicator.
86
+ def new_indicator(indicator_class)
87
+ indicator_class.new(series:, source:).tap do |indicator|
88
+ add_dominant_cycle_indicator(indicator.dominant_cycle_indicator_class, indicator)
89
+ add_dependent_indicators(indicator_class.dependent_indicator_classes, indicator)
90
+ add_indicator(indicator_class, indicator)
91
+ end
92
+ end
93
+
94
+ # Adds a new indicator to the collection of indicators. Once added, every
95
+ # tick added to the series triggers the indicator's compute to fire.
96
+ # The ordered indicators list is adjusted after adding the new indicator.
97
+ def add_indicator(indicator_class, new_indicator)
98
+ return if indicators[indicator_class]
99
+
100
+ indicators[indicator_class] = new_indicator
101
+ @ordered_indicators = (ordered_indicators << new_indicator).sort_by(&:priority)
102
+ new_indicator
103
+ end
104
+
105
+ # Adds dependent indicators to the indicator collection. This method is reentrant and
106
+ # will also add depencies of the dependent indicators.
107
+ # Dependent indicators automatically adjust priority based on the dependency.
108
+ def add_dependent_indicators(indicator_classes, indicator)
109
+ return if indicator_classes.empty?
110
+
111
+ # Dependent indicators should come after dominant cycle indicators, but before the
112
+ # indicators that depend on them.
113
+ dependency_priority = (Quant::Indicators::Indicator::DEPENDENCY_PRIORITY + indicator.priority) / 2
114
+
115
+ indicator_classes.each_with_index do |indicator_class, index|
116
+ next if indicators[indicator_class]
117
+
118
+ new_indicator = indicator_class.new(series:, source:)
119
+ new_indicator.define_singleton_method(:priority) { dependency_priority + index }
120
+ add_dependent_indicators(indicator_class.dependent_indicator_classes, new_indicator)
121
+ add_indicator(indicator_class, new_indicator)
122
+ end
123
+ end
124
+
125
+ # Adds the dominant cycle indicator to the collection of indicators. Indicators added
126
+ # by this method must be a subclass of {Quant::Indicators::DominantCycles::DominantCycle}.
127
+ def add_dominant_cycle_indicator(dominant_cycle_class, indicator)
128
+ return if indicator.is_a?(Indicators::DominantCycles::DominantCycle)
129
+ return unless dominant_cycle_class
130
+ return if indicators[dominant_cycle_class]
131
+
132
+ dominant_cycle = dominant_cycle_class.new(series:, source:)
133
+ add_indicator(dominant_cycle_class, dominant_cycle)
134
+ end
135
+
136
+ def invalid_source_error(source:)
137
+ raise InvalidIndicatorSource, "Invalid indicator source: #{source.inspect}"
138
+ end
139
+ end
140
+ end
@@ -1,28 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quant
4
+ # {Quant::IndicatorSources} pairs a collection of {Quant::Indicators::Indicator} with an input source.
5
+ # This allows us to only compute indicators for the sources that are referenced at run-time.
6
+ # Any source explicitly used at run-time will have its indicator computed and only those indicators
7
+ # will be computed.
4
8
  class IndicatorsSources
9
+ ALL_SOURCES = [
10
+ PRICE_SOURCES = %i[price open_price high_price low_price close_price].freeze,
11
+ VOLUME_SOURCES = %i[volume base_volume target_volume].freeze,
12
+ COMPUTED_SOURCES = %i[oc2 hl2 hlc3 ohlc4].freeze
13
+ ].flatten.freeze
14
+
15
+ attr_reader :series
16
+
5
17
  def initialize(series:)
6
18
  @series = series
7
- @indicator_sources = {}
8
- end
9
-
10
- def new_indicator(indicator)
11
- @indicator_sources[indicator.source] ||= Indicators.new(series: @series, source: indicator.source)
19
+ @sources = {}
12
20
  end
13
21
 
14
22
  def [](source)
15
- return @indicator_sources[source] if @indicator_sources.key?(source)
23
+ raise invalid_source_error(source:) unless ALL_SOURCES.include?(source)
16
24
 
17
- raise Quant::Errors::InvalidIndicatorSource, "Invalid source, #{source.inspect}."
25
+ @sources[source] ||= IndicatorsSource.new(series:, source:)
18
26
  end
19
27
 
20
28
  def <<(tick)
21
- @indicator_sources.each_value { |indicator| indicator << tick }
29
+ @sources.each_value { |indicator| indicator << tick }
30
+ end
31
+
32
+ ALL_SOURCES.each do |source|
33
+ define_method(source) do
34
+ @sources[source] ||= IndicatorsSource.new(series:, source:)
35
+ end
22
36
  end
23
37
 
24
- def oc2
25
- @indicator_sources[:oc2] ||= Indicators.new(series: @series, source: :oc2)
38
+ def respond_to_missing?(method, *)
39
+ oc2.respond_to?(method)
40
+ end
41
+
42
+ def method_missing(method_name, *args, &block)
43
+ return super unless respond_to_missing?(method_name)
44
+
45
+ oc2.send(method_name, *args, &block)
46
+ end
47
+
48
+ private
49
+
50
+ def invalid_source_error(source:)
51
+ raise Errors::InvalidIndicatorSource, "Invalid indicator source: #{source.inspect}"
26
52
  end
27
53
  end
28
54
  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
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ class PivotsSource
5
+ def initialize(indicator_source:)
6
+ @indicator_source = indicator_source
7
+ end
8
+
9
+ def atr; indicator(Indicators::Pivots::Atr) end
10
+ def bollinger; indicator(Indicators::Pivots::Bollinger) end
11
+ def camarilla; indicator(Indicators::Pivots::Camarilla) end
12
+ def classic; indicator(Indicators::Pivots::Classic) end
13
+ def demark; indicator(Indicators::Pivots::Demark) end
14
+ def donchian; indicator(Indicators::Pivots::Donchian) end
15
+ def fibbonacci; indicator(Indicators::Pivots::Fibbonacci) end
16
+ def guppy; indicator(Indicators::Pivots::Guppy) end
17
+ def keltner; indicator(Indicators::Pivots::Keltner) end
18
+ def murrey; indicator(Indicators::Pivots::Murrey) end
19
+ def traditional; indicator(Indicators::Pivots::Traditional) end
20
+ def woodie; indicator(Indicators::Pivots::Woodie) end
21
+
22
+ private
23
+
24
+ def indicator(indicator_class)
25
+ @indicator_source[indicator_class]
26
+ end
27
+ end
28
+ end
@@ -103,6 +103,7 @@ module Quant
103
103
 
104
104
  # Computes the mean of the array. When +n+ is specified, the mean is computed over
105
105
  # the last +n+ elements, otherwise it is computed over the entire array.
106
+ # If the array is empty, 0.0 is returned.
106
107
  #
107
108
  # @param n [Integer] the number of elements to compute the mean over
108
109
  # @return [Float]
@@ -113,6 +114,19 @@ module Quant
113
114
  subset.sum / subset.size.to_f
114
115
  end
115
116
 
117
+ # Returns the largest absolute value in the array. When +n+ is specified, the peak is computed over
118
+ # the last +n+ elements, otherwise it is computed over the entire array.
119
+ # If the array is empty, 0.0 is returned.
120
+ #
121
+ # @param n [Integer] the number of elements to compute the peak over
122
+ # @return [Float]
123
+ def peak(n: size)
124
+ subset = last(n)
125
+ return 0.0 if subset.empty?
126
+
127
+ [subset.max.abs, subset.min.abs].max
128
+ end
129
+
116
130
  # Computes the Exponential Moving Average (EMA) of the array. When +n+ is specified,
117
131
  # the EMA is computed over the last +n+ elements, otherwise it is computed over the entire array.
118
132
  # An Array of EMA's is returned, with the first entry always the first value in the subset.
data/lib/quant/series.rb CHANGED
@@ -60,18 +60,20 @@ module Quant
60
60
  @ticks = []
61
61
  end
62
62
 
63
- def limit_iterations(start_iteration, stop_iteration)
64
- selected_ticks = ticks[start_iteration..stop_iteration]
63
+ def build_limited_series(selected_ticks)
65
64
  return self if selected_ticks.size == ticks.size
66
65
 
67
66
  self.class.from_ticks(symbol:, interval:, ticks: selected_ticks)
68
67
  end
69
68
 
69
+ def limit_iterations(start_iteration, stop_iteration)
70
+ selected_ticks = ticks[start_iteration..stop_iteration]
71
+ build_limited_series selected_ticks
72
+ end
73
+
70
74
  def limit(period)
71
75
  selected_ticks = ticks.select{ |tick| period.cover?(tick.close_timestamp) }
72
- return self if selected_ticks.size == ticks.size
73
-
74
- self.class.from_ticks(symbol:, interval:, ticks: selected_ticks)
76
+ build_limited_series selected_ticks
75
77
  end
76
78
 
77
79
  def_delegator :@ticks, :[]
@@ -80,6 +82,7 @@ module Quant
80
82
  def_delegator :@ticks, :select!
81
83
  def_delegator :@ticks, :reject!
82
84
  def_delegator :@ticks, :last
85
+ def_delegator :@ticks, :take
83
86
 
84
87
  def highest
85
88
  ticks.max_by(&:high_price)
@@ -101,20 +104,6 @@ module Quant
101
104
  "#<#{self.class.name} symbol=#{symbol} interval=#{interval} ticks=#{ticks.size}>"
102
105
  end
103
106
 
104
- # When the first indicator is instantiated, it will also lead to instantiating
105
- # the dominant cycle indicator. The `new_indicator_lock` prevents reentrant calls
106
- # to the `new_indicator` method with infinite recursion.
107
- def new_indicator(indicator)
108
- return if @new_indicator_lock
109
-
110
- begin
111
- @new_indicator_lock = true
112
- indicators.new_indicator(indicator)
113
- ensure
114
- @new_indicator_lock = false
115
- end
116
- end
117
-
118
107
  def <<(tick)
119
108
  tick = Ticks::Spot.new(price: tick) if tick.is_a?(Numeric)
120
109
  indicators << tick unless tick.series?
@@ -59,6 +59,7 @@ module Quant
59
59
  @half_period = settings[:half_period] || compute_half_period
60
60
  @micro_period = settings[:micro_period] || Settings::MICRO_PERIOD
61
61
 
62
+ @dominant_cycle_indicator_class = nil
62
63
  @dominant_cycle_kind = settings[:dominant_cycle_kind] || Settings::DOMINANT_CYCLE_KINDS.first
63
64
  @pivot_kind = settings[:pivot_kind] || Settings::PIVOT_KINDS.first
64
65
  end
@@ -68,6 +69,7 @@ module Quant
68
69
  @min_period = settings.fetch(:min_period, @min_period)
69
70
  compute_half_period
70
71
  @micro_period = settings.fetch(:micro_period, @micro_period)
72
+ @dominant_cycle_indicator_class = nil
71
73
  @dominant_cycle_kind = settings.fetch(:dominant_cycle_kind, @dominant_cycle_kind)
72
74
  @pivot_kind = settings.fetch(:pivot_kind, @pivot_kind)
73
75
  end
@@ -83,6 +85,15 @@ module Quant
83
85
  def compute_half_period
84
86
  @half_period = (max_period + min_period) / 2
85
87
  end
88
+
89
+ def dominant_cycle_indicator_class
90
+ return @dominant_cycle_indicator_class if @dominant_cycle_indicator_class
91
+
92
+ base_class_name = dominant_cycle_kind.to_s.split("_").map(&:capitalize).join
93
+ class_name = "Quant::Indicators::DominantCycles::#{base_class_name}"
94
+
95
+ @dominant_cycle_indicator_class = Object.const_get(class_name)
96
+ end
86
97
  end
87
98
  end
88
99
  end
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.3.0"
5
5
  end
data/possibilities.png ADDED
Binary file
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/quant/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "quantitative"
7
+ spec.version = Quant::VERSION
8
+ spec.authors = ["Michael Lang"]
9
+ spec.email = ["mwlang@cybrains.net"]
10
+
11
+ spec.summary = "Quantitative and statistical tools written for Ruby 3.2+ for trading and finance."
12
+ spec.description = spec.summary
13
+ spec.homepage = "https://github.com/mwlang/quantitative"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.2"
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = spec.homepage
21
+ spec.metadata["changelog_uri"] = spec.homepage
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
28
+ end
29
+ end
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ # Uncomment to register a new dependency of your gem
35
+ spec.add_dependency "oj", "~> 3.10"
36
+
37
+ # For more information and examples about making a new gem, check out our
38
+ # guide at: https://bundler.io/guides/creating_gem.html
39
+ end