quantitative 0.1.4 → 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.
@@ -1,46 +1,40 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quant
2
4
  class Indicators
3
5
  class MaPoint < IndicatorPoint
6
+ attribute :ss, key: "ss"
7
+ attribute :ema, key: "ema"
4
8
  attr_accessor :ss, :ema, :osc
5
9
 
6
- def to_h
7
- {
8
- "ss" => ss,
9
- "ema" => delta_phase,
10
- "osc" => osc
11
- }
12
- end
13
-
14
- def initialize_data_points(indicator:)
15
- @ss = oc2
16
- @ema = oc2
10
+ def initialize_data_points
11
+ @ss = input
12
+ @ema = input
17
13
  @osc = nil
18
14
  end
19
15
  end
20
16
 
21
17
  # Moving Averages
22
18
  class Ma < Indicator
23
- def self.indicator_key
24
- "ma"
25
- end
19
+ include Quant::Mixins::Filters
26
20
 
27
21
  def alpha(period)
28
22
  bars_to_alpha(period)
29
23
  end
30
24
 
31
25
  def min_period
32
- settings.min_period
26
+ 8 # Quant.config.indicators.min_period
33
27
  end
34
28
 
35
- def period
36
- settings.max_period
29
+ def max_period
30
+ 48 # Quant.config.indicators.max_period
37
31
  end
38
32
 
39
33
  def compute
40
- p0.ss = super_smoother p0.oc2, :ss, min_period
41
- p0.ema = alpha(period) * p0.oc2 + (1 - alpha(period)) * p1.ema
34
+ # p0.ss = super_smoother input, :ss, min_period
35
+ p0.ema = alpha(max_period) * input + (1 - alpha(max_period)) * p1.ema
42
36
  p0.osc = p0.ss - p0.ema
43
37
  end
44
38
  end
45
39
  end
46
- end
40
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
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.
9
+ class PingPoint < IndicatorPoint
10
+ attribute :pong
11
+ attribute :compute_count, default: 0
12
+ end
13
+
14
+ # A simple idicator used primarily to test the indicator system
15
+ class Ping < Indicator
16
+ def compute
17
+ p0.pong = input
18
+ p0.compute_count += 1
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,29 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quant
4
- # < IndicatorsAccessor
4
+ # TODO: build an Indicator registry so new indicators can be added and used outside those shipped with the library.
5
5
  class Indicators
6
- # def atr; indicator(Indicators::Atr) end
7
- # def adx; indicator(Indicators::Adx) end
8
- # def cci; indicator(Indicators::Cci) end
9
- # def cdi; indicator(Indicators::Cdi) end
10
- # def decycler; indicator(Indicators::Decycler) end
11
- # def frema; indicator(Indicators::Frema) end
12
- # def hilo; indicator(Indicators::HiLo) end
13
- # def ma; indicator(Indicators::Ma) end
14
- # def mama; indicator(Indicators::Mama) end
15
- # def frama; indicator(Indicators::Frama) end
16
- # def mesa; indicator(Indicators::Mesa) end
17
- # def roofing; indicator(Indicators::Roofing) end
18
- # def rsi; indicator(Indicators::Rsi) end
19
- # def rrr; indicator(Indicators::Rrr) end
20
- # def rrsi; indicator(Indicators::RocketRsi) end
21
- # def samo; indicator(Indicators::Samo) end
22
- # def snr; indicator(Indicators::Snr) end
23
- # def ssf; indicator(Indicators::Ssf) end
24
- # def volume; indicator(Indicators::VolumeSsf) end
25
- # def vol; indicator(Indicators::Vol) end
26
- # def vrsi; indicator(Indicators::VolumeRsi) end
27
- # def weibull; indicator(Indicators::Weibull) end
28
6
  end
29
7
  end
@@ -0,0 +1,61 @@
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, :indicators
17
+
18
+ def initialize(series:, source:)
19
+ @series = series
20
+ @source = source
21
+ @indicators = {}
22
+ end
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.
27
+ def indicator(indicator_class)
28
+ indicators[indicator_class] ||= indicator_class.new(series: series, source: source)
29
+ end
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.
38
+ def <<(tick)
39
+ indicators.each_value { |indicator| indicator << tick }
40
+ end
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
+
58
+ def ma; indicator(Indicators::Ma) end
59
+ def ping; indicator(Indicators::Ping) end
60
+ end
61
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ class IndicatorsSources
5
+ def initialize(series:)
6
+ @series = series
7
+ @indicator_sources = {}
8
+ end
9
+
10
+ def <<(tick)
11
+ @indicator_sources.each_value { |indicator| indicator << tick }
12
+ end
13
+
14
+ def oc2
15
+ @indicator_sources[:oc2] ||= IndicatorsProxy.new(series: @series, source: :oc2)
16
+ end
17
+ end
18
+ end
@@ -39,13 +39,13 @@
39
39
  # pp series
40
40
  #
41
41
  module Quant
42
- # +Quant::Interval+ abstracts away the concept of ticks (candles, bars, etc.) and their duration and offers some basic utilities for
43
- # working with multiple timeframes. Intervals are used in +Tick+ and +Series+ classes to define the duration of the ticks.
42
+ # {Quant::Interval} abstracts away the concept of ticks (candles, bars, etc.) and their duration and offers some basic utilities for
43
+ # working with multiple timeframes. Intervals are used in {Quant::Ticks::Tick} and {Quant::Series} classes to define the duration of the ticks.
44
44
  #
45
- # When the +Interval+ is unknown, it is set to +'na'+ (not available) and the duration is set to 0. The shorthand for this is
45
+ # When the {Quant::Interval} is unknown, it is set to +'na'+ (not available) and the duration is set to 0. The shorthand for this is
46
46
  # +Interval.na+. and +Interval[:na]+. and +Interval[nil]+.
47
47
  #
48
- # +Interval+ are instantiated in multple ways to support a wide variety of use-cases. Here's an example:
48
+ # {Quant::Interval} are instantiated in multple ways to support a wide variety of use-cases. Here's an example:
49
49
  # Quant::Interval.new("1d") # => #<Quant::Interval @interval="1d"> (daily interval)
50
50
  # Quant::Interval.new(:daily) # => #<Quant::Interval @interval="1d">
51
51
  # Quant::Interval[:daily] # => #<Quant::Interval @interval="1d">
@@ -122,7 +122,7 @@ module Quant
122
122
 
123
123
  # Instantiates an Interval from a resolution. For example, TradingView uses resolutions
124
124
  # like "1", "3", "5", "15", "30", "60", "240", "D", "1D" to represent the duration of a
125
- # candlestick. +from_resolution+ translates resolutions to the appropriate +Interval+.
125
+ # candlestick. +from_resolution+ translates resolutions to the appropriate {Quant::Interval}.
126
126
  def self.from_resolution(resolution)
127
127
  ensure_valid_resolution!(resolution)
128
128
 
@@ -130,7 +130,7 @@ module Quant
130
130
  end
131
131
 
132
132
  # Instantiates an Interval from a string or symbol. If the value is already
133
- # an +Interval+, it is returned as-is.
133
+ # an {Quant::Interval}, it is returned as-is.
134
134
  def self.[](value)
135
135
  return value if value.is_a? Interval
136
136
 
@@ -153,19 +153,28 @@ module Quant
153
153
  @interval = (interval || "na").to_s
154
154
  end
155
155
 
156
+ # Returns true when the duration of the interval is zero, such as for the `na` interval.
156
157
  def nil?
157
- interval == "na"
158
+ duration.zero?
158
159
  end
159
160
 
160
161
  def to_s
161
162
  interval
162
163
  end
163
164
 
165
+ # Returns the total span of seconds or duration for the interval.
166
+ # @example
167
+ # Quant::Interval.new("1d").duration => 86400
168
+ # Quant::Interval.new("1h").duration => 3600
169
+ # Quant::Interval.new("1m").duration => 60
170
+ # Quant::Interval.new("1s").duration => 1
171
+ # Quant::Interval.new("na").duration => 0
164
172
  def duration
165
173
  INTERVAL_DISTANCE[interval]
166
174
  end
167
175
  alias seconds duration
168
176
 
177
+ # Compares the interval to another interval, string, or symbol and returns true if they are equal.
169
178
  def ==(other)
170
179
  if other.is_a? String
171
180
  interval.to_s == other
@@ -176,13 +185,22 @@ module Quant
176
185
  end
177
186
  end
178
187
 
188
+ # Returns the number of ticks this interval represents per minute.
189
+ # @example
190
+ # Quant::Interval.new("1d").ticks_per_minute => 0.0006944444444444445
191
+ # Quant::Interval.new("1h").ticks_per_minute => 0.016666666666666666
192
+ # Quant::Interval.new("1m").ticks_per_minute => 1.0
193
+ # Quant::Interval.new("1s").ticks_per_minute => 60.0
179
194
  def ticks_per_minute
180
195
  60.0 / seconds
181
196
  end
182
197
 
198
+ # Returns the half-life of the interval in seconds.
199
+ # @example
200
+ # Quant::Interval.new("1d").half_life => 43200.0
201
+ # Quant::Interval.new("1h").half_life => 1800.0
202
+ # Quant::Interval.new("1m").half_life => 30.0
183
203
  def half_life
184
- raise "bad interval #{interval}" if duration.nil?
185
-
186
204
  duration / 2.0
187
205
  end
188
206
 
@@ -193,11 +211,16 @@ module Quant
193
211
  Interval.new intervals[intervals.index(interval) + 1] || intervals[-1]
194
212
  end
195
213
 
214
+ # Returns the full list of valid interval Strings that can be used to instantiate an {Quant::Interval}.
196
215
  def self.valid_intervals
197
216
  INTERVAL_DISTANCE.keys
198
217
  end
199
218
 
200
- # NOTE: if timestamp doesn't cover a full interval, it will be rounded up to 1
219
+ # Computes the number of ticks from present to given timestamp.
220
+ # If timestamp doesn't cover a full interval, it will be rounded up to 1
221
+ # @example
222
+ # interval = Quant::Interval.new("1d")
223
+ # interval.ticks_to(Time.now + 5.days) # => 5 NOTE: `5.days` is an ActiveSupport method
201
224
  def ticks_to(timestamp)
202
225
  ((timestamp - Quant.current_time) / duration).round(2).ceil
203
226
  end
@@ -209,7 +232,8 @@ module Quant
209
232
  def self.ensure_valid_resolution!(resolution)
210
233
  return if RESOLUTIONS.keys.include? resolution
211
234
 
212
- raise InvalidResolution, "resolution (#{resolution}) not a valid resolution. Should be one of: (#{RESOLUTIONS.keys.join(", ")})"
235
+ should_be_one_of = "Should be one of: (#{RESOLUTIONS.keys.join(", ")})"
236
+ raise Errors::InvalidResolution, "resolution (#{resolution}) not a valid resolution. #{should_be_one_of}"
213
237
  end
214
238
 
215
239
  private
@@ -221,7 +245,8 @@ module Quant
221
245
  def ensure_valid_interval!(interval)
222
246
  return if interval.nil? || valid_intervals.include?(interval.to_s)
223
247
 
224
- raise InvalidInterval, "interval (#{interval.inspect}) not a valid interval. Should be one of: (#{valid_intervals.join(", ")})"
248
+ should_be_one_of = "Should be one of: (#{valid_intervals.join(", ")})"
249
+ raise Errors::InvalidInterval, "interval (#{interval.inspect}) not a valid interval. #{should_be_one_of}"
225
250
  end
226
251
 
227
252
  def ensure_valid_resolution!(resolution)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "trig"
4
+
3
5
  module Quant
4
6
  module Mixins
5
7
  # 1. All the common filters useful for traders have a transfer response that can be written
@@ -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,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quant
2
4
  module Refinements
3
- # Refinements for the standard Ruby +Array+ class.
5
+ # Refinements for the standard Ruby {Quant::Array} class.
4
6
  # These refinements add statistical methods to the Array class as well as some optimizations that greatly
5
7
  # speed up some of the computations performed by the various indicators.
6
8
  #
@@ -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)
@@ -67,7 +68,7 @@ module Quant
67
68
  end
68
69
 
69
70
  # Treats the tail of the array as starting at zero and counting up. Does not overflow the head of the array.
70
- # That is, if the +Array+ has 5 elements, prev(10) would return the first element in the array.
71
+ # That is, if the {Quant::Array} has 5 elements, prev(10) would return the first element in the array.
71
72
  #
72
73
  # @example
73
74
  # series = [1, 2, 3, 4]
@@ -89,11 +90,12 @@ module Quant
89
90
  # +max_size+, the first element is removed from the array.
90
91
  # This setting modifies :<< and :push methods.
91
92
  def max_size!(max_size)
92
- # These guards are maybe not necessary, but they are here until a use-case is found.
93
- # My concern lies with indicators that are built specifically against the +max_size+ of a given array.
94
- raise Quant::ArrayMaxSizeError, 'cannot set max_size to nil.' unless max_size
95
- raise Quant::ArrayMaxSizeError, 'can only max_size! once.' if @max_size
96
- raise Quant::ArrayMaxSizeError, "size of Array #{size} exceeds max_size #{max_size}." if size > max_size
93
+ # These guards are maybe unnecessary, but they are here until a use-case is found.
94
+ # Some indicators are built specifically against the +max_size+ of a given array.
95
+ # Adjusting the +max_size+ after the fact could lead to unexpected, unintended behavior.
96
+ raise Errors::ArrayMaxSizeError, "Cannot set max_size to nil." unless max_size
97
+ raise Errors::ArrayMaxSizeError, "The max_size can only be set once." if @max_size
98
+ raise Errors::ArrayMaxSizeError, "The size of Array #{size} exceeds max_size #{max_size}." if size > max_size
97
99
 
98
100
  @max_size = max_size
99
101
  self
@@ -108,7 +110,7 @@ module Quant
108
110
  subset = last(n)
109
111
  return 0.0 if subset.empty?
110
112
 
111
- sum = subset.sum / subset.size.to_f
113
+ subset.sum / subset.size.to_f
112
114
  end
113
115
 
114
116
  # Computes the Exponential Moving Average (EMA) of the array. When +n+ is specified,
@@ -174,7 +176,7 @@ module Quant
174
176
  # @param n [Integer] the number of elements to compute the Standard Deviation over.
175
177
  # @return [Float]
176
178
  def stddev(reference_value, n: size)
177
- variance(reference_value, n: n) ** 0.5
179
+ variance(reference_value, n: n)**0.5
178
180
  end
179
181
 
180
182
  def variance(reference_value, n: size)
data/lib/quant/series.rb CHANGED
@@ -3,34 +3,47 @@
3
3
  module Quant
4
4
  # Ticks belong to the first series they're associated with always.
5
5
  # There are no provisions for series merging their ticks to one series!
6
- # Indicators will be computed against the parent series of a list of ticks, so we
6
+ # {Indicators} will be computed against the parent series of a list of ticks, so we
7
7
  # can safely work with subsets of a series and indicators will compute just once.
8
8
  class Series
9
9
  include Enumerable
10
10
  extend Forwardable
11
11
 
12
- def self.from_file(filename:, symbol:, interval:, folder: nil)
13
- symbol = symbol.to_s.upcase
14
- interval = Interval[interval]
15
-
16
- filename = Rails.root.join("historical", folder, "#{symbol.upcase}.txt") if filename.nil?
12
+ # Loads a series of ticks when each line is a parsible JSON string that represents a tick.
13
+ # A {Quant::Ticks::TickSerializer} may be passed to convert the parsed JSON to {Quant::Ticks::Tick} object.
14
+ # @param filename [String] The filename to load the ticks from.
15
+ # @param symbol [String] The symbol of the series.
16
+ # @param interval [String] The interval of the series.
17
+ # @param serializer_class [Class] {Quant::Ticks::TickSerializer} class to use for the conversion.
18
+ def self.from_file(filename:, symbol:, interval:, serializer_class: nil)
17
19
  raise "File #{filename} does not exist" unless File.exist?(filename)
18
20
 
19
- lines = File.read(filename).split("\n")
20
- ticks = lines.map{ |line| Quant::Ticks::OHLC.from_json(line) }
21
-
22
- from_ticks(symbol: symbol, interval: interval, ticks: ticks)
21
+ ticks = File.read(filename).split("\n").map{ |line| Oj.load(line) }
22
+ from_hash symbol: symbol, interval: interval, hash: ticks, serializer_class: serializer_class
23
23
  end
24
24
 
25
- def self.from_json(symbol:, interval:, json:)
26
- from_hash symbol: symbol, interval: interval, hash: Oj.load(json)
25
+ # Loads a series of ticks when the JSON string represents an array of ticks.
26
+ # A {Quant::Ticks::TickSerializer} may be passed to convert the parsed JSON to {Quant::Ticks::Tick} object.
27
+ # @param symbol [String] The symbol of the series.
28
+ # @param interval [String] The interval of the series.
29
+ # @param json [String] The JSON string to parse into ticks.
30
+ # @param serializer_class [Class] {Quant::Ticks::TickSerializer} class to use for the conversion.
31
+ def self.from_json(symbol:, interval:, json:, serializer_class: nil)
32
+ ticks = Oj.load(json)
33
+ from_hash symbol: symbol, interval: interval, hash: ticks, serializer_class: serializer_class
27
34
  end
28
35
 
29
- def self.from_hash(symbol:, interval:, hash:)
30
- ticks = hash.map { |tick_hash| Quant::Ticks::OHLC.from(tick_hash) }
31
- from_ticks(symbol: symbol, interval: interval, ticks: ticks)
36
+ # Loads a series of ticks where the hash must be cast to an array of {Quant::Ticks::Tick} objects.
37
+ # @param symbol [String] The symbol of the series.
38
+ # @param interval [String] The interval of the series.
39
+ # @param hash [Array<Hash>] The array of hashes to convert to {Quant::Ticks::Tick} objects.
40
+ # @param serializer_class [Class] {Quant::Ticks::TickSerializer} class to use for the conversion.
41
+ def self.from_hash(symbol:, interval:, hash:, serializer_class: nil)
42
+ ticks = hash.map { |tick_hash| Quant::Ticks::OHLC.from(tick_hash, serializer_class: serializer_class) }
43
+ from_ticks symbol: symbol, interval: interval, ticks: ticks
32
44
  end
33
45
 
46
+ # Loads a series of ticks where the array represents an array of {Quant::Ticks::Tick} objects.
34
47
  def self.from_ticks(symbol:, interval:, ticks:)
35
48
  ticks = ticks.sort_by(&:close_timestamp)
36
49
 
@@ -43,7 +56,7 @@ module Quant
43
56
 
44
57
  def initialize(symbol:, interval:)
45
58
  @symbol = symbol
46
- @interval = interval
59
+ @interval = Interval[interval]
47
60
  @ticks = []
48
61
  end
49
62
 
@@ -64,11 +77,8 @@ module Quant
64
77
  def_delegator :@ticks, :[]
65
78
  def_delegator :@ticks, :size
66
79
  def_delegator :@ticks, :each
67
- def_delegator :@ticks, :select
68
80
  def_delegator :@ticks, :select!
69
- def_delegator :@ticks, :reject
70
81
  def_delegator :@ticks, :reject!
71
- def_delegator :@ticks, :first
72
82
  def_delegator :@ticks, :last
73
83
 
74
84
  def highest
@@ -92,7 +102,14 @@ module Quant
92
102
  end
93
103
 
94
104
  def <<(tick)
105
+ tick = Ticks::Spot.new(price: tick) if tick.is_a?(Numeric)
106
+ indicators << tick unless tick.series?
95
107
  @ticks << tick.assign_series(self)
108
+ self
109
+ end
110
+
111
+ def indicators
112
+ @indicators ||= IndicatorsSources.new(series: self)
96
113
  end
97
114
 
98
115
  def to_h
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quant
2
4
  module Settings
3
5
  MAX_PERIOD = 48
@@ -4,14 +4,15 @@ require_relative "tick"
4
4
 
5
5
  module Quant
6
6
  module Ticks
7
- # An +OHLC+ is a bar or candle for a point in time that has an open, high, low, and close price.
8
- # It is the most common form of a +Tick+ and is usually used to representa time period such as a
9
- # minute, hour, day, week, or month. The +OHLC+ is used to represent the price action of an asset
10
- # The interval of the +OHLC+ is the time period that the +OHLC+ represents, such has hourly, daily, weekly, etc.
7
+ # An {Quant::Ticks::OHLC} is a bar or candle for a point in time that has an open, high, low, and close price.
8
+ # It is the most common form of a {Quant::Ticks::Tick} and is usually used to representa time period such as a
9
+ # minute, hour, day, week, or month. The {Quant::Ticks::OHLC} is used to represent the price action of an asset
10
+ # The interval of the {Quant::Ticks::OHLC} is the time period that the {Quant::Ticks::OHLC} represents,
11
+ # such has hourly, daily, weekly, etc.
11
12
  class OHLC < Tick
12
13
  include TimeMethods
13
14
 
14
- attr_reader :interval, :series
15
+ attr_reader :series
15
16
  attr_reader :close_timestamp, :open_timestamp
16
17
  attr_reader :open_price, :high_price, :low_price, :close_price
17
18
  attr_reader :base_volume, :target_volume, :trades
@@ -26,8 +27,6 @@ module Quant
26
27
  low_price:,
27
28
  close_price:,
28
29
 
29
- interval: nil,
30
-
31
30
  volume: nil,
32
31
  base_volume: nil,
33
32
  target_volume: nil,
@@ -44,8 +43,6 @@ module Quant
44
43
  @low_price = low_price.to_f
45
44
  @close_price = close_price.to_f
46
45
 
47
- @interval = Interval[interval]
48
-
49
46
  @base_volume = (volume || base_volume).to_i
50
47
  @target_volume = (target_volume || @base_volume).to_i
51
48
  @trades = trades.to_i
@@ -92,7 +89,7 @@ module Quant
92
89
  # This method is useful for comparing the volatility of different assets.
93
90
  # @return [Float]
94
91
  def daily_price_change_ratio
95
- @price_change ||= ((open_price - close_price) / oc2).abs
92
+ @daily_price_change_ratio ||= ((open_price - close_price) / oc2).abs
96
93
  end
97
94
 
98
95
  # Set the #green? property to true when the close_price is greater than or equal to the open_price.
@@ -131,7 +128,7 @@ module Quant
131
128
  end
132
129
 
133
130
  def inspect
134
- "#<#{self.class.name} #{interval} ct=#{close_timestamp.iso8601} o=#{open_price} h=#{high_price} l=#{low_price} c=#{close_price} v=#{volume}>"
131
+ "#<#{self.class.name} ct=#{close_timestamp.iso8601} o=#{open_price} h=#{high_price} l=#{low_price} c=#{close_price} v=#{volume}>"
135
132
  end
136
133
  end
137
134
  end