quantitative 0.1.4 → 0.1.6

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