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
@@ -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
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "high_pass_filters"
4
- require_relative "butterworth_filters"
5
- require_relative "universal_filters"
6
3
  module Quant
7
4
  module Mixins
8
5
  module Filters
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "weighted_moving_average"
4
- require_relative "simple_moving_average"
5
- require_relative "exponential_moving_average"
6
3
  module Quant
7
4
  module Mixins
8
5
  module MovingAverages
@@ -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
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "tick"
4
-
5
3
  module Quant
6
4
  module Ticks
7
5
  # A {Quant::Ticks::OHLC} is a bar or candle for a point in time that
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "tick"
4
-
5
3
  module Quant
6
4
  module Ticks
7
5
  # A +Spot+ is a single price point in time. It is the most basic form of a {Quant::Ticks::Tick} and is usually used to represent
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.2"
4
+ VERSION = "0.3.1"
5
5
  end
data/lib/quantitative.rb CHANGED
@@ -4,14 +4,31 @@ require "time"
4
4
  require "date"
5
5
  require "oj"
6
6
  require "csv"
7
+ require "zeitwerk"
7
8
 
8
9
  lib_folder = File.expand_path(File.join(File.dirname(__FILE__)))
9
10
  quant_folder = File.join(lib_folder, "quant")
10
11
 
11
- # require top-level files
12
- Dir.glob(File.join(quant_folder, "*.rb")).each { |fn| require fn }
12
+ # Explicitly require module functions since Zeitwerk isn't configured, yet.
13
+ require_relative "quant/time_methods"
14
+ require_relative "quant/config"
15
+ require_relative "quant/experimental"
16
+ module Quant
17
+ include TimeMethods
18
+ include Config
19
+ include Experimental
20
+ end
21
+
22
+ # Configure Zeitwerk to autoload the Quant module.
23
+ loader = Zeitwerk::Loader.for_gem
24
+ loader.push_dir(quant_folder, namespace: Quant)
25
+
26
+ loader.inflector.inflect "ohlc" => "OHLC"
27
+ loader.inflector.inflect "version" => "VERSION"
28
+
29
+ loader.setup
13
30
 
14
- # require sub-folders and their sub-folders
15
- %w(refinements mixins statistics settings ticks indicators).each do |sub_folder|
31
+ # Refinements aren't autoloaded by Zeitwerk, so we need to require them manually.
32
+ %w(refinements).each do |sub_folder|
16
33
  Dir.glob(File.join(quant_folder, sub_folder, "**/*.rb")).each { |fn| require fn }
17
34
  end
data/possibilities.png ADDED
Binary file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quantitative
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Lang
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-17 00:00:00.000000000 Z
11
+ date: 2024-06-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '3.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: zeitwerk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.6'
27
41
  description: Quantitative and statistical tools written for Ruby 3.2+ for trading
28
42
  and finance.
29
43
  email:
@@ -48,7 +62,7 @@ files:
48
62
  - lib/quant/asset_class.rb
49
63
  - lib/quant/attributes.rb
50
64
  - lib/quant/config.rb
51
- - lib/quant/dominant_cycle_indicators.rb
65
+ - lib/quant/dominant_cycles_source.rb
52
66
  - lib/quant/errors.rb
53
67
  - lib/quant/experimental.rb
54
68
  - lib/quant/indicators.rb
@@ -69,7 +83,20 @@ files:
69
83
  - lib/quant/indicators/mama.rb
70
84
  - lib/quant/indicators/mesa.rb
71
85
  - lib/quant/indicators/ping.rb
72
- - lib/quant/indicators_proxy.rb
86
+ - lib/quant/indicators/pivot.rb
87
+ - lib/quant/indicators/pivots/atr.rb
88
+ - lib/quant/indicators/pivots/bollinger.rb
89
+ - lib/quant/indicators/pivots/camarilla.rb
90
+ - lib/quant/indicators/pivots/classic.rb
91
+ - lib/quant/indicators/pivots/demark.rb
92
+ - lib/quant/indicators/pivots/donchian.rb
93
+ - lib/quant/indicators/pivots/fibbonacci.rb
94
+ - lib/quant/indicators/pivots/guppy.rb
95
+ - lib/quant/indicators/pivots/keltner.rb
96
+ - lib/quant/indicators/pivots/murrey.rb
97
+ - lib/quant/indicators/pivots/traditional.rb
98
+ - lib/quant/indicators/pivots/woodie.rb
99
+ - lib/quant/indicators_source.rb
73
100
  - lib/quant/indicators_sources.rb
74
101
  - lib/quant/interval.rb
75
102
  - lib/quant/mixins/butterworth_filters.rb
@@ -86,6 +113,7 @@ files:
86
113
  - lib/quant/mixins/super_smoother.rb
87
114
  - lib/quant/mixins/universal_filters.rb
88
115
  - lib/quant/mixins/weighted_moving_average.rb
116
+ - lib/quant/pivots_source.rb
89
117
  - lib/quant/refinements/array.rb
90
118
  - lib/quant/series.rb
91
119
  - lib/quant/settings.rb
@@ -101,6 +129,7 @@ files:
101
129
  - lib/quant/time_period.rb
102
130
  - lib/quant/version.rb
103
131
  - lib/quantitative.rb
132
+ - possibilities.png
104
133
  homepage: https://github.com/mwlang/quantitative
105
134
  licenses:
106
135
  - MIT
@@ -124,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
124
153
  - !ruby/object:Gem::Version
125
154
  version: '0'
126
155
  requirements: []
127
- rubygems_version: 3.5.6
156
+ rubygems_version: 3.5.11
128
157
  signing_key:
129
158
  specification_version: 4
130
159
  summary: Quantitative and statistical tools written for Ruby 3.2+ for trading and
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Quant
4
- # The {Quant::IndicatorsProxy} class is responsible for lazily loading indicators
5
- # so that not all indicators are always engaged and computing their values.
6
- # If the indicator is never accessed, it's never computed, saving valuable
7
- # processing CPU cycles.
8
- #
9
- # Indicators are generally built around the concept of a source input value and
10
- # that source is designated by the source parameter when instantiating the
11
- # {Quant::IndicatorsProxy} class.
12
- #
13
- # By design, the {Quant::Indicator} class holds the {Quant::Ticks::Tick} instance
14
- # alongside the indicator's computed values for that tick.
15
- class IndicatorsProxy
16
- attr_reader :series, :source, :dominant_cycle, :indicators
17
-
18
- def initialize(series:, source:)
19
- @series = series
20
- @source = source
21
- @indicators = {}
22
- @dominant_cycle = dominant_cycle_indicator
23
- end
24
-
25
- def dominant_cycle_indicator
26
- kind = Quant.config.indicators.dominant_cycle_kind.to_s
27
- base_class_name = kind.split("_").map(&:capitalize).join
28
- class_name = "Quant::Indicators::DominantCycles::#{base_class_name}"
29
- indicator_class = Object.const_get(class_name)
30
- indicator_class.new(series:, source:)
31
- end
32
-
33
- # Instantiates the indicator class and stores it in the indicators hash. Once
34
- # prepared, the indicator becomes active and all ticks pushed into the series
35
- # are sent to the indicator for processing.
36
- def indicator(indicator_class)
37
- indicators[indicator_class] ||= indicator_class.new(series:, source:)
38
- end
39
-
40
- # Adds the tick to all active indicators, triggering them to compute
41
- # new values against the latest tick.
42
- #
43
- # NOTE: Dominant cycle indicators must be computed first as many
44
- # indicators are adaptive and require the dominant cycle period.
45
- # The IndicatorsProxy class is not responsible for enforcing
46
- # this order of events.
47
- def <<(tick)
48
- dominant_cycle << tick
49
- indicators.each_value { |indicator| indicator << tick }
50
- end
51
-
52
- # Attaches a given Indicator class and defines the method for
53
- # accessing it using the given name. Indicators take care of
54
- # computing their values when first attached to a populated
55
- # series.
56
- #
57
- # The indicators shipped with the library are all wired into the framework, thus
58
- # this method should be used for custom indicators not shipped with the library.
59
- #
60
- # @param name [Symbol] The name of the method to define for accessing the indicator.
61
- # @param indicator_class [Class] The class of the indicator to attach.
62
- # @example
63
- # series.indicators.oc2.attach(name: :foo, indicator_class: Indicators::Foo)
64
- def attach(name:, indicator_class:)
65
- define_singleton_method(name) { indicator(indicator_class) }
66
- end
67
- end
68
- end