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
@@ -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