quantitative 0.1.5 → 0.1.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9fa0c537499409fa9faa3fbca76beca44d37205c04c8de1c01b3e681985c2dc
4
- data.tar.gz: 3916820207b47d2d19b79332e41329c35080ce1456c4a2ed751863ea220b5289
3
+ metadata.gz: 72d65b65fdcbb81650fcce3233f3fbde9c99c4b9b9ace1ce3bbc3b8e565152f9
4
+ data.tar.gz: f687ab3c32e88ffc579a9f92ff428846f91e60ff166d3b4379963deb15a88425
5
5
  SHA512:
6
- metadata.gz: e3bead0b7fc23208c62dd18bb8f701b684db20780ae793fab973ab07cea0740897026a66755a07b9857228e815bfc3679801171f2b5875f510a7c98781fd30cb
7
- data.tar.gz: 8b07dbcbe1a86c507a31df950aef293ae018913964848a9298310fe71a7929924a5a7ced417cf5ef86e9282feb58bc9d16531bf6b77e0fa23cea28de7a761740
6
+ metadata.gz: 594d57a819ce8d561c064f685353d27ef4dfd1cb2e76e5cf07758bcedb1c501d2bc06859aff89cf6f4bc744e327ede1290e31b4d4ffb1daed99d614735eb0a76
7
+ data.tar.gz: 5fbfb29d1501a3562788abf83ef4fcb78bcb17fcfc030e7543b42f3c29c2a7a5c3c8fba187494d4655691e117e383af59bca428764f49fd7e41fcef911ef6d40
data/.rubocop.yml CHANGED
@@ -3,6 +3,8 @@ inherit_gem:
3
3
 
4
4
  AllCops:
5
5
  TargetRubyVersion: 3.0
6
+ Exclude:
7
+ - 'spec/performance/*.rb'
6
8
 
7
9
  Style/AccessorGrouping:
8
10
  Enabled: false
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quantitative (0.1.5)
4
+ quantitative (0.1.6)
5
5
  oj (~> 3.10)
6
6
 
7
7
  GEM
@@ -1,10 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quant
4
+ # {Quant::Attributes} is similar to an +attr_accessor+ definition. It provides a simple DSL
5
+ # for defining attributes or properies on an {Quant::Indicators::IndicatorPoint} class.
6
+ #
7
+ # {Quant::Attributes} tracks all defined attributes from child to parent classes,
8
+ # allowing child classes to inherit their parent's attributes as well as redefine them.
9
+ #
10
+ # The exception on redefining is that a serialized key cannot be redefined. Experience
11
+ # has proven that this leads to serialization surprises where what was written to a specific
12
+ # key is not what was expected!
13
+ #
14
+ # NOTE: The above design constraint could be improved with a force or overwrite option.
15
+ #
16
+ # If :default is an immediate value (Integer, Float, Boolean, etc.), it will be used as the
17
+ # initial value for the attribute. If :default is a Symbol, it will send a message on
18
+ # current instance of the class get the default value.
19
+ #
20
+ # @example
21
+ # class FooPoint < IndicatorPoint
22
+ # # will not serialize to a key
23
+ # attribute :bar
24
+ # # serializes to "bzz" key
25
+ # attribute :baz, key: "bzz"
26
+ # # calls the random method on the instance for the default value
27
+ # attribute :foobar, default: :random
28
+ # # delegated to the tick's high_price method
29
+ # attribute :high, default: :high_price
30
+ # # calls the lambda bound to instance for default
31
+ # attribute :low, default: -> { high_price - 5 }
32
+ #
33
+ # def random
34
+ # rand(100)
35
+ # end
36
+ # end
37
+ #
38
+ # class BarPoint < FooPoint
39
+ # attribute :bar, key: "brr" # redefines and sets the key for bar
40
+ # attribute :qux, key: "qxx", default: 5.0 # serializes to "qxx" and defaults to 5.0
41
+ # end
42
+ #
43
+ # FooPoint.attributes
44
+ # # => { bar: { key: nil, default: nil },
45
+ # baz: { key: "bzz", default: nil } }
46
+ #
47
+ # BarPoint.attributes
48
+ # # => { bar: { key: "brr", default: nil },
49
+ # # baz: { key: "bzz", default: nil },
50
+ # # qux: { key: "qxx", default: nil } }
51
+ #
52
+ # BarPoint.new.bar # => nil
53
+ # BarPoint.new.qux # => 5.0
54
+ # BarPoint.new.bar = 2.0 => 2.0
4
55
  module Attributes
5
- # Tracks all defined attributes, allowing child classes to inherit their parent's attributes.
6
- # The registry key is the class registering an attrbritute and is itself a hash of the attribute name
7
- # and the attribute's key and default value.
56
+ # The +registry+ key is the class registering an attrbritute and is itself
57
+ # a hash of the attribute name and the attribute's key and default value.
8
58
  # Internal use only.
9
59
  #
10
60
  # @example
@@ -47,9 +97,18 @@ module Quant
47
97
  end
48
98
 
49
99
  module InstanceMethods
50
- def initialize(...)
51
- initialize_attributes
52
- super(...)
100
+ # Makes some assumptions about the class's initialization having a +tick+ keyword argument.
101
+ #
102
+ # The challenge here is that we prepend this module to the class, and we are
103
+ # initializing attributes before the owning class gets the opportunity to initialize
104
+ # variables that we wanted to depend on with being able to define a default
105
+ # value that could set default values from a +tick+ method.
106
+ #
107
+ # Ok for now. May need to be more flexible in the future. Alternative strategy could be
108
+ # to lazy eval the default value the first time it is accessed.
109
+ def initialize(*args, **kwargs)
110
+ initialize_attributes(tick: kwargs[:tick])
111
+ super(*args, **kwargs)
53
112
  end
54
113
 
55
114
  # Iterates over all defined attributes in a child => parent hierarchy,
@@ -65,17 +124,42 @@ module Quant
65
124
  end
66
125
  end
67
126
 
127
+ # The default value can be one of the following:
128
+ # - A symbol that is a method on the instance responds to
129
+ # - A symbol that is a method that the instance's tick responds to
130
+ # - A Proc that is bound to the instance
131
+ # - An immediate value (Integer, Float, Boolean, etc.)
132
+ def default_value_for(entry, new_tick)
133
+ # let's not assume tick is always available/implemented
134
+ # can get from instance or from initializer passed here as `new_tick`
135
+ current_tick = new_tick
136
+ current_tick ||= tick if respond_to?(:tick)
137
+
138
+ if entry[:default].is_a?(Symbol) && respond_to?(entry[:default])
139
+ send(entry[:default])
140
+
141
+ elsif entry[:default].is_a?(Symbol) && current_tick&.respond_to?(entry[:default])
142
+ current_tick.send(entry[:default])
143
+
144
+ elsif entry[:default].is_a?(Proc)
145
+ instance_exec(&entry[:default])
146
+
147
+ else
148
+ entry[:default]
149
+ end
150
+ end
151
+
68
152
  # Initializes the defined attributes with default values and
69
153
  # defines accessor methods for each attribute.
70
154
  # If a child class redefines a parent's attribute, the child's
71
155
  # definition will be used.
72
- def initialize_attributes
156
+ def initialize_attributes(tick:)
73
157
  each_attribute do |name, entry|
74
158
  # use the child's definition, skipping the parent's
75
159
  next if respond_to?(name)
76
160
 
77
161
  ivar_name = "@#{name}"
78
- instance_variable_set(ivar_name, entry[:default])
162
+ instance_variable_set(ivar_name, default_value_for(entry, tick))
79
163
  define_singleton_method(name) { instance_variable_get(ivar_name) }
80
164
  define_singleton_method("#{name}=") { |value| instance_variable_set(ivar_name, value) }
81
165
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quant
2
4
  class Indicators
3
5
  class Indicator
@@ -27,6 +29,10 @@ module Quant
27
29
  @points.keys
28
30
  end
29
31
 
32
+ def [](index)
33
+ values[index]
34
+ end
35
+
30
36
  def values
31
37
  @points.values
32
38
  end
@@ -59,7 +65,7 @@ module Quant
59
65
  end
60
66
 
61
67
  def inspect
62
- "#<#{self.class.name} symbol=#{series.symbol} source=#{source} #{points.size} ticks>"
68
+ "#<#{self.class.name} symbol=#{series.symbol} source=#{source} #{ticks.size} ticks>"
63
69
  end
64
70
 
65
71
  def compute
@@ -168,4 +174,4 @@ module Quant
168
174
  # end
169
175
  end
170
176
  end
171
- end
177
+ end
@@ -5,7 +5,7 @@ module Quant
5
5
  class IndicatorPoint
6
6
  include Quant::Attributes
7
7
 
8
- attribute :tick
8
+ attr_reader :tick
9
9
  attribute :source, key: "src"
10
10
  attribute :input, key: "in"
11
11
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quant
2
4
  class Indicators
3
5
  class MaPoint < IndicatorPoint
@@ -1,5 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quant
2
4
  class Indicators
5
+ # A simple point used primarily to test the indicator system in unit tests.
6
+ # It has a simple computation that just sets the pong value to the input value
7
+ # and increments the compute_count by 1 each time compute is called.
8
+ # Sometimes you just gotta play ping pong to win.
3
9
  class PingPoint < IndicatorPoint
4
10
  attribute :pong
5
11
  attribute :compute_count, default: 0
@@ -1,6 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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.
4
15
  class IndicatorsProxy
5
16
  attr_reader :series, :source, :indicators
6
17
 
@@ -10,14 +21,41 @@ module Quant
10
21
  @indicators = {}
11
22
  end
12
23
 
24
+ # Instantiates the indicator class and stores it in the indicators hash. Once
25
+ # prepared, the indicator becomes active and all ticks pushed into the series
26
+ # are sent to the indicator for processing.
13
27
  def indicator(indicator_class)
14
28
  indicators[indicator_class] ||= indicator_class.new(series: series, source: source)
15
29
  end
16
30
 
31
+ # Adds the tick to all active indicators, triggering them to compute
32
+ # new values against the latest tick.
33
+ #
34
+ # NOTE: Dominant cycle indicators must be computed first as many
35
+ # indicators are adaptive and require the dominant cycle period.
36
+ # The IndicatorsProxy class is not responsible for enforcing
37
+ # this order of events.
17
38
  def <<(tick)
18
39
  indicators.each_value { |indicator| indicator << tick }
19
40
  end
20
41
 
42
+ # Attaches a given Indicator class and defines the method for
43
+ # accessing it using the given name. Indicators take care of
44
+ # computing their values when first attached to a populated
45
+ # series.
46
+ #
47
+ # The indicators shipped with the library are all wired into the framework, thus
48
+ # this method should be used for custom indicators not shipped with the library.
49
+ #
50
+ # @param name [Symbol] The name of the method to define for accessing the indicator.
51
+ # @param indicator_class [Class] The class of the indicator to attach.
52
+ # @example
53
+ # series.indicators.oc2.attach(name: :foo, indicator_class: Indicators::Foo)
54
+ def attach(name:, indicator_class:)
55
+ define_singleton_method(name) { indicator(indicator_class) }
56
+ end
57
+
21
58
  def ma; indicator(Indicators::Ma) end
59
+ def ping; indicator(Indicators::Ping) end
22
60
  end
23
61
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Mixins
5
+ module MovingAverages
6
+ using Quant
7
+
8
+ # Computes the Weighted Moving Average (WMA) of the series, using the four most recent data points.
9
+ #
10
+ # @param source [Symbol] the source of the data points to be used in the calculation.
11
+ # @return [Float] the weighted average of the series.
12
+ # @raise [ArgumentError] if the source is not a Symbol.
13
+ # @example
14
+ # p0.wma = weighted_average(:close_price)
15
+ def weighted_moving_average(source)
16
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
17
+
18
+ [4.0 * p0.send(source),
19
+ 3.0 * p1.send(source),
20
+ 2.0 * p2.send(source),
21
+ p3.send(source)].sum / 10.0
22
+ end
23
+ alias wma weighted_moving_average
24
+
25
+ # Computes the Weighted Moving Average (WMA) of the series, using the seven most recent data points.
26
+ #
27
+ # @param source [Symbol] the source of the data points to be used in the calculation.
28
+ # @return [Float] the weighted average of the series.
29
+ # @raise [ArgumentError] if the source is not a Symbol.
30
+ # @example
31
+ # p0.wma = weighted_average(:close_price)
32
+ def extended_weighted_moving_average(source)
33
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
34
+
35
+ [7.0 * p0.send(source),
36
+ 6.0 * p1.send(source),
37
+ 5.0 * p2.send(source),
38
+ 4.0 * p3.send(source),
39
+ 3.0 * p(4).send(source),
40
+ 2.0 * p(5).send(source),
41
+ p(6).send(source)].sum / 28.0
42
+ end
43
+ alias ewma extended_weighted_moving_average
44
+
45
+ # Computes the Simple Moving Average (SMA) of the given period.
46
+ #
47
+ # @param source [Symbol] the source of the data points to be used in the calculation.
48
+ # @param period [Integer] the number of elements to compute the SMA over.
49
+ # @return [Float] the simple moving average of the period.
50
+ def simple_moving_average(source, period:)
51
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
52
+
53
+ values.last(period).map { |value| value.send(source) }.mean
54
+ end
55
+ alias sma simple_moving_average
56
+
57
+ # Computes the Exponential Moving Average (EMA) of the given period.
58
+ #
59
+ # The EMA computation is optimized to compute using just the last two
60
+ # indicator data points and is expected to be called in each indicator's
61
+ # `#compute` method for each iteration on the series.
62
+ #
63
+ # @param source [Symbol] the source of the data points to be used in the calculation.
64
+ # @param previous [Symbol] the previous EMA value.
65
+ # @param period [Integer] the number of elements to compute the EMA over.
66
+ # @return [Float] the exponential moving average of the period.
67
+ # @raise [ArgumentError] if the source is not a Symbol.
68
+ # @example
69
+ # def compute
70
+ # p0.ema = exponential_moving_average(:close_price, period: 3)
71
+ # end
72
+ #
73
+ # def compute
74
+ # p0.ema = exponential_moving_average(:close_price, previous: :ema, period: 3)
75
+ # end
76
+ def exponential_moving_average(source, previous: :ema, period:)
77
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
78
+ raise ArgumentError, "previous must be a Symbol" unless previous.is_a?(Symbol)
79
+
80
+ alpha = 2.0 / (period + 1)
81
+ p0.send(source) * alpha + p1.send(previous) * (1.0 - alpha)
82
+ end
83
+ alias ema exponential_moving_average
84
+ end
85
+ end
86
+ end
@@ -66,9 +66,8 @@ module Quant
66
66
  p3 = points[-4] || p2
67
67
 
68
68
  v0 = source.is_a?(Symbol) ? p0.send(source) : source
69
- return v0 if [p0 == p3]
69
+ return v0 if p0 == p3
70
70
 
71
- debugger if points.size > 4
72
71
  a1 = Math.exp(-Math::PI / ssperiod)
73
72
  b1 = 2 * a1 * Math.cos(Math::PI * Math.sqrt(3) / ssperiod)
74
73
  c1 = a1**2
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quant
2
4
  module Refinements
3
5
  # Refinements for the standard Ruby {Quant::Array} class.
@@ -32,7 +34,6 @@ module Quant
32
34
  # The refined behavior generally only exists within the library's scope, but if you call `using Quant` in your
33
35
  # own code, you may encounter the changed behavior unexpectedly.
34
36
  module Array
35
-
36
37
  # Overrides the standard +<<+ method to track the +maximum+ and +minimum+ values
37
38
  # while also respecting the +max_size+ setting.
38
39
  def <<(value)
@@ -109,7 +110,7 @@ module Quant
109
110
  subset = last(n)
110
111
  return 0.0 if subset.empty?
111
112
 
112
- sum = subset.sum / subset.size.to_f
113
+ subset.sum / subset.size.to_f
113
114
  end
114
115
 
115
116
  # Computes the Exponential Moving Average (EMA) of the array. When +n+ is specified,
@@ -175,7 +176,7 @@ module Quant
175
176
  # @param n [Integer] the number of elements to compute the Standard Deviation over.
176
177
  # @return [Float]
177
178
  def stddev(reference_value, n: size)
178
- variance(reference_value, n: n) ** 0.5
179
+ variance(reference_value, n: n)**0.5
179
180
  end
180
181
 
181
182
  def variance(reference_value, n: size)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quant
2
4
  module Settings
3
5
  MAX_PERIOD = 48
@@ -89,7 +89,7 @@ module Quant
89
89
  # This method is useful for comparing the volatility of different assets.
90
90
  # @return [Float]
91
91
  def daily_price_change_ratio
92
- @price_change ||= ((open_price - close_price) / oc2).abs
92
+ @daily_price_change_ratio ||= ((open_price - close_price) / oc2).abs
93
93
  end
94
94
 
95
95
  # Set the #green? property to true when the close_price is greater than or equal to the open_price.
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.1.5"
4
+ VERSION = "0.1.6"
5
5
  end
data/lib/quantitative.rb CHANGED
@@ -14,4 +14,4 @@ Dir.glob(File.join(quant_folder, "*.rb")).each { |fn| require fn }
14
14
  # require sub-folders and their sub-folders
15
15
  %w(refinements mixins settings ticks indicators).each do |sub_folder|
16
16
  Dir.glob(File.join(quant_folder, sub_folder, "**/*.rb")).each { |fn| require fn }
17
- end
17
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quantitative
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Lang
@@ -62,10 +62,10 @@ files:
62
62
  - lib/quant/mixins/fisher_transform.rb
63
63
  - lib/quant/mixins/high_pass_filter.rb
64
64
  - lib/quant/mixins/hilbert_transform.rb
65
+ - lib/quant/mixins/moving_averages.rb
65
66
  - lib/quant/mixins/stochastic.rb
66
67
  - lib/quant/mixins/super_smoother.rb
67
68
  - lib/quant/mixins/trig.rb
68
- - lib/quant/mixins/weighted_average.rb
69
69
  - lib/quant/refinements/array.rb
70
70
  - lib/quant/series.rb
71
71
  - lib/quant/settings.rb
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Quant
4
- module Mixins
5
- module WeightedAverage
6
- def weighted_average(source)
7
- value = source.is_a?(Symbol) ? p0.send(source) : source
8
- [4.0 * value,
9
- 3.0 * p1.send(source),
10
- 2.0 * p2.send(source),
11
- p3.send(source),].sum / 10.0
12
- end
13
-
14
- def extended_weighted_average(source)
15
- value = source.is_a?(Symbol) ? p0.send(source) : source
16
- [7.0 * value,
17
- 6.0 * p1.send(source),
18
- 5.0 * p2.send(source),
19
- 4.0 * p3.send(source),
20
- 3.0 * prev(4).send(source),
21
- 2.0 * prev(5).send(source),
22
- prev(6).send(source),].sum / 28.0
23
- end
24
- end
25
- end
26
- end