quantitative 0.1.5 → 0.1.6

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