quantitative 0.1.0 → 0.1.2

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: 81321ddd563920c047db7eef1c0eaa922a582a5fc17f0c52ea49b4a629aa9010
4
- data.tar.gz: d7e00ef24cf0dcf1aaf2cb015591058e37fbbf2d0771f8d950f6d7e38da662cc
3
+ metadata.gz: 19fe65cd5c50b0b503abaa0e7c5be3949a284cf5a2b537d79b9cbdd69448041d
4
+ data.tar.gz: fe03ed6b2ddef6609cee31547cd84c1eac948bb8d10efb8c24c316673c693a62
5
5
  SHA512:
6
- metadata.gz: f79190f22f241256fe8a34c35ad8289c09253642a3f534ac8ee4b1fc3fc4cf5f092bbc6c263adb0bf9087067fb5772ad74ff8430f623b467e76973a7a0e95b16
7
- data.tar.gz: fa39ccd339097b6d7f18b0e5e9707643be4c224d52b5a27b0adea8b292f626f48ee4c7058771ca4316674196506920fd202b671c40446235e9d121bd9b2ea22e
6
+ metadata.gz: 9ce1d965a8e68f6fae76143dfc619ed98c8a5af559e9f26c67dd7685e60bf9b4cfa3ecf5f43908f01597862cb6d6389cebd40cf5897afed560a9e54ed79ea91e
7
+ data.tar.gz: ee8527e43aa4670175d20a20c6037af11227071ff0fe738344c4b9b91d2391335d210a9e5b305153e78edc75ca9acb802b56b2502223107a84366ae40ba55b7f
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.0
data/.yardopts ADDED
@@ -0,0 +1,7 @@
1
+ --protected
2
+ --no-private
3
+ lib/quant/**/*.rb
4
+ -
5
+ CODE_OF_CONDUCT.md
6
+ DISCLAIMER.md
7
+ LICENSE
data/Gemfile CHANGED
@@ -11,4 +11,10 @@ gem "rspec", "~> 3.0"
11
11
 
12
12
  gem "rubocop", "~> 1.21"
13
13
  gem "rubocop-rspec"
14
- gem "relaxed-rubocop"
14
+ gem "relaxed-rubocop"
15
+
16
+ gem "debug"
17
+ gem "guard-rspec", "~> 4.7"
18
+ gem "yard", "~> 0.9"
19
+ gem "benchmark-ips", "~> 2.9"
20
+
data/Gemfile.lock CHANGED
@@ -1,13 +1,15 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quantitative (0.1.0)
4
+ quantitative (0.1.2)
5
5
  oj (~> 3.10)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
10
  ast (2.4.2)
11
+ benchmark-ips (2.13.0)
12
+ bigdecimal (3.1.6)
11
13
  coderay (1.1.3)
12
14
  debug (1.8.0)
13
15
  irb (>= 1.5.0)
@@ -44,7 +46,8 @@ GEM
44
46
  notiffany (0.1.3)
45
47
  nenv (~> 0.1)
46
48
  shellany (~> 0.0)
47
- oj (3.16.1)
49
+ oj (3.16.3)
50
+ bigdecimal (>= 3.0)
48
51
  parallel (1.24.0)
49
52
  parser (3.3.0.4)
50
53
  ast (~> 2.4.1)
@@ -112,6 +115,7 @@ PLATFORMS
112
115
  arm64-darwin-22
113
116
 
114
117
  DEPENDENCIES
118
+ benchmark-ips (~> 2.9)
115
119
  debug
116
120
  guard-rspec (~> 4.7)
117
121
  quantitative!
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Quantitative
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/quantitative.svg)](https://badge.fury.io/rb/quantitative)
4
+
3
5
  STATUS: ALPHA - very early stages! The framework is very much a work in progress and I am rapidly introducing new things and changing existing things around.
4
6
 
5
7
  Quantitative is a statistical and quantitative library for Ruby 3.x for trading stocks, cryptocurrency, and forex. It provides a number of classes and modules for working with time-series data, financial data, and other quantitative data. It is designed to be fast, efficient, and easy to use.
data/lib/quant/errors.rb CHANGED
@@ -4,4 +4,5 @@ module Quant
4
4
  class Error < StandardError; end
5
5
  class InvalidInterval < Error; end
6
6
  class InvalidResolution < Error; end
7
+ class ArrayMaxSizeError < Error; end
7
8
  end
@@ -167,7 +167,13 @@ module Quant
167
167
  alias seconds duration
168
168
 
169
169
  def ==(other)
170
- interval == other&.interval
170
+ if other.is_a? String
171
+ interval.to_s == other
172
+ elsif other.is_a? Symbol
173
+ interval == MAPPINGS[other]&.fetch(:interval, nil)
174
+ else
175
+ interval == other&.interval
176
+ end
171
177
  end
172
178
 
173
179
  def ticks_per_minute
@@ -0,0 +1,196 @@
1
+ module Quant
2
+ module Refinements
3
+ # Refinements for the standard Ruby +Array+ class.
4
+ # These refinements add statistical methods to the Array class as well as some optimizations that greatly
5
+ # speed up some of the computations performed by the various indicators.
6
+ #
7
+ # In addtion to the statistical methods, the refinements also add a +max_size!+ method to the Array class, which
8
+ # allows us to bound the array to a maximum number of elements, which is useful for indicators that are computing
9
+ # averages or sums over a fixed number of lookback ticks.
10
+ #
11
+ # There are some various performance benchmarks in the spec/performance directory that show the performance
12
+ # improvements of using these refinements.
13
+ #
14
+ # Keep in mind that within this library, we're generally concerned with adding to the tail of the arrays and
15
+ # rarely with removing or popping, so there's few optimizations or protections for those operations in
16
+ # conjunction with the +max_size+ setting. The +max_size+ has also been designed to be set only once, to avoid
17
+ # adding additional complexity to the code that is unnecessary until a use-case presents itself.
18
+ #
19
+ # Usage: Call +using Quant+ in the file or scope where you want to use these refinements. It does not matter
20
+ # if the arrays were instantiated outside the scope of the refinement, the refinements will still be applied.
21
+ #
22
+ # @example
23
+ # using Quant
24
+ #
25
+ # array = [1, 2, 3, 4, 5]
26
+ # array.mean => 3.0
27
+ # another_array.max_size!(3).push(1, 2, 3, 4, 5, 6) => [4, 5, 6]
28
+ #
29
+ # @note The behavior of out of bound indexes into the Array deviates from standard Ruby and always returns an element.
30
+ # If an array only has three elements and 4 or more are requested for +n+, the method constrains itself to
31
+ # the size of the array. This is an intentional design decision, but it may be a gotcha if you're not expecting it.
32
+ # The refined behavior generally only exists within the library's scope, but if you call `using Quant` in your
33
+ # own code, you may encounter the changed behavior unexpectedly.
34
+ module Array
35
+
36
+ # Overrides the standard +<<+ method to track the +maximum+ and +minimum+ values
37
+ # while also respecting the +max_size+ setting.
38
+ def <<(value)
39
+ push(value)
40
+ end
41
+
42
+ # Overrides the standard +push+ method to track the +maximum+ and +minimum+ values
43
+ # while also respecting the +max_size+ setting.
44
+ def push(*objects)
45
+ Array(objects).each do |object|
46
+ super(object)
47
+ if @max_size && size > @max_size
48
+ voted_off = shift
49
+ @minimum = min if voted_off == @minimum
50
+ @maximum = max if voted_off == @maximum
51
+ else
52
+ @maximum = object if @maximum.nil? || object > @maximum
53
+ @minimum = object if @minimum.nil? || object < @minimum
54
+ end
55
+ end
56
+ self
57
+ end
58
+
59
+ # Returns the maximum element value in the array. It is an optimized version of the standard +max+ method.
60
+ def maximum
61
+ @maximum || max
62
+ end
63
+
64
+ # Returns the minimum element value in the array. It is an optimized version of the standard +min+ method.
65
+ def minimum
66
+ @minimum || min
67
+ end
68
+
69
+ # 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
+ #
72
+ # @example
73
+ # series = [1, 2, 3, 4]
74
+ # series.prev(0) # => series[-1] => series[3] => 4
75
+ # series.prev(1) # => series[-2] => series[3] => 3
76
+ # series.prev(2) # => series[-3] => series[2] => 2
77
+ # series.prev(3) # => series[-4] => series[1] => 1
78
+ # series.prev(4) # => series[-4] => series[0] => 1 (no out of bounds!)
79
+ #
80
+ # Useful for when translating TradingView or MQ4 indicators to Ruby where those programs' indexing starts at 0
81
+ # for most recent bar and counts up to the oldest bar.
82
+ def prev(index)
83
+ raise ArgumentError, "index must be positive" if index < 0
84
+
85
+ self[[size - (index + 1), 0].max]
86
+ end
87
+
88
+ # Sets the maximum size of the array. When the size of the array exceeds the
89
+ # +max_size+, the first element is removed from the array.
90
+ # This setting modifies :<< and :push methods.
91
+ 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
97
+
98
+ @max_size = max_size
99
+ self
100
+ end
101
+
102
+ # Computes the mean of the array. When +n+ is specified, the mean is computed over
103
+ # the last +n+ elements, otherwise it is computed over the entire array.
104
+ #
105
+ # @param n [Integer] the number of elements to compute the mean over
106
+ # @return [Float]
107
+ def mean(n: size)
108
+ subset = last(n)
109
+ return 0.0 if subset.empty?
110
+
111
+ sum = subset.sum / subset.size.to_f
112
+ end
113
+
114
+ # Computes the Exponential Moving Average (EMA) of the array. When +n+ is specified,
115
+ # the EMA is computed over the last +n+ elements, otherwise it is computed over the entire array.
116
+ # An Array of EMA's is returned, with the first entry always the first value in the subset.
117
+ #
118
+ # @params n [Integer] the number of elements to compute the EMA over.
119
+ # @return [Array<Float>]
120
+ def ema(n: size)
121
+ subset = last(n)
122
+ return [] if subset.empty?
123
+
124
+ alpha = 2.0 / (subset.size + 1)
125
+ naught_alpha = (1.0 - alpha)
126
+ pvalue = subset[0]
127
+ subset.map do |value|
128
+ pvalue = (alpha * value) + (naught_alpha * pvalue)
129
+ end
130
+ end
131
+
132
+ # Computes the Simple Moving Average (SMA) of the array. When +n+ is specified,
133
+ # the SMA is computed over the last +n+ elements, otherwise it is computed over the entire array.
134
+ # An Array of SMA's is returned, with the first entry always the first value in the subset.
135
+ #
136
+ # @param n [Integer] the number of elements to compute the SMA over
137
+ # @return [Array<Float>]
138
+ def sma(n: size)
139
+ subset = last(n)
140
+ return [] if subset.empty?
141
+
142
+ pvalue = subset[0]
143
+ subset.map do |value|
144
+ pvalue = (pvalue + value) / 2.0
145
+ end
146
+ end
147
+
148
+ # Computes the Weighted Moving Average (WMA) of the array. When +n+ is specified,
149
+ # the WMA is computed over the last +n+ elements, otherwise it is computed over the entire array.
150
+ # An Array of WMA's is returned, with the first entry always the first value in the subset.
151
+ #
152
+ # @param n [Integer] the number of elements to compute the WMA over
153
+ # @return [Array<Float>]
154
+ def wma(n: size)
155
+ subset = last(n)
156
+ return [] if subset.empty?
157
+
158
+ # ensures we return not more than number of elements in full array,
159
+ # yet have enough values to compute each iteration
160
+ max_size = [size, n].min
161
+ while subset.size <= max_size + 2
162
+ subset.unshift(subset[0])
163
+ end
164
+
165
+ subset.each_cons(4).map do |v1, v2, v3, v4|
166
+ (4.0 * v4 + 3.0 * v3 + 2.0 * v2 + v1) / 10.0
167
+ end
168
+ end
169
+
170
+ # Computes the Standard Deviation of the array. When +n+ is specified,
171
+ # the Standard Deviation is computed over the last +n+ elements,
172
+ # otherwise it is computed over the entire array.
173
+ #
174
+ # @param n [Integer] the number of elements to compute the Standard Deviation over.
175
+ # @return [Float]
176
+ def stddev(reference_value, n: size)
177
+ variance(reference_value, n: n) ** 0.5
178
+ end
179
+
180
+ def variance(reference_value, n: size)
181
+ subset = last(n)
182
+ return 0.0 if subset.empty?
183
+
184
+ subset.empty? ? 0.0 : subset.map{ |v| (v - reference_value)**2 }.mean
185
+ end
186
+ end
187
+ end
188
+
189
+ refine Array do
190
+ import_methods Quant::Refinements::Array
191
+
192
+ alias std_dev stddev
193
+ alias standard_deviation stddev
194
+ alias var variance
195
+ end
196
+ end
@@ -1,125 +1,98 @@
1
- require_relative 'value'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tick"
2
4
 
3
5
  module Quant
4
6
  module Ticks
5
- # serialized keys
6
- # ot: open timestamp
7
- # ct: close timestamp
8
- # iv: interval
9
-
10
- # o: open price
11
- # h: high price
12
- # l: low price
13
- # c: close price
14
-
15
- # bv: base volume
16
- # tv: target volume
17
- # ct: close timestamp
18
-
19
- # t: trades
20
- # g: green
21
- # j: doji
22
- class OHLC < Value
23
- def self.from(hash)
24
- new \
25
- open_timestamp: hash["ot"],
26
- close_timestamp: hash["ct"],
27
- interval: hash["iv"],
28
-
29
- open_price: hash["o"],
30
- high_price: hash["h"],
31
- low_price: hash["l"],
32
- close_price: hash["c"],
33
-
34
- base_volume: hash["bv"],
35
- target_volume: hash["tv"],
36
-
37
- trades: hash["t"],
38
- green: hash["g"],
39
- doji: hash["j"]
40
- end
41
-
42
- def self.from_json(json)
43
- from Oj.load(json)
44
- end
45
-
46
- def initialize(open_timestamp:,
47
- close_timestamp:,
48
- interval: nil,
49
-
50
- open_price:,
51
- high_price:,
52
- low_price:,
53
- close_price:,
54
-
55
- base_volume: 0.0,
56
- target_volume: 0.0,
57
-
58
- trades: 0,
59
- green: false,
60
- doji: nil)
61
-
62
- super(price: close_price, timestamp: close_timestamp, interval:, trades:)
7
+ class OHLC < Tick
8
+ include TimeMethods
9
+
10
+ attr_reader :interval, :series
11
+ attr_reader :close_timestamp, :open_timestamp
12
+ attr_reader :open_price, :high_price, :low_price, :close_price
13
+ attr_reader :base_volume, :target_volume, :trades
14
+ attr_reader :green, :doji
15
+
16
+ def initialize(
17
+ open_timestamp:,
18
+ close_timestamp:,
19
+
20
+ open_price:,
21
+ high_price:,
22
+ low_price:,
23
+ close_price:,
24
+
25
+ interval: nil,
26
+
27
+ volume: nil,
28
+ base_volume: nil,
29
+ target_volume: nil,
30
+
31
+ trades: nil,
32
+ green: nil,
33
+ doji: nil
34
+ )
63
35
  @open_timestamp = extract_time(open_timestamp)
36
+ @close_timestamp = extract_time(close_timestamp)
37
+
64
38
  @open_price = open_price.to_f
65
39
  @high_price = high_price.to_f
66
40
  @low_price = low_price.to_f
41
+ @close_price = close_price.to_f
42
+
43
+ @interval = Interval[interval]
67
44
 
68
- @base_volume = base_volume.to_i
69
- @target_volume = target_volume.to_i
45
+ @base_volume = (volume || base_volume).to_i
46
+ @target_volume = (target_volume || @base_volume).to_i
47
+ @trades = trades.to_i
70
48
 
71
49
  @green = green.nil? ? compute_green : green
72
50
  @doji = doji.nil? ? compute_doji : doji
51
+ super()
73
52
  end
74
53
 
54
+ alias price close_price
55
+ alias timestamp close_timestamp
56
+ alias volume base_volume
57
+
75
58
  def hl2; ((high_price + low_price) / 2.0) end
76
59
  def oc2; ((open_price + close_price) / 2.0) end
77
60
  def hlc3; ((high_price + low_price + close_price) / 3.0) end
78
61
  def ohlc4; ((open_price + high_price + low_price + close_price) / 4.0) end
79
62
 
63
+ # The corresponding? method helps determine that the other tick's timestamp is the same as this tick's timestamp,
64
+ # which is useful when aligning ticks between two separate series where one starts or ends at a different time,
65
+ # or when there may be gaps in the data between the two series.
80
66
  def corresponding?(other)
81
67
  [open_timestamp, close_timestamp] == [other.open_timestamp, other.close_timestamp]
82
68
  end
83
69
 
84
- # percent change from open to close
85
- def delta
86
- ((open_price / close_price) - 1.0) * 100
70
+ # Returns the percent daily price change from open_price to close_price, ranging from 0.0 to 1.0.
71
+ # A positive value means the price increased, and a negative value means the price decreased.
72
+ # A value of 0.0 means no change.
73
+ # @return [Float]
74
+ def daily_price_change
75
+ ((open_price / close_price) - 1.0)
76
+ rescue ZeroDivisionError
77
+ 0.0
87
78
  end
88
79
 
89
- def to_h
90
- { "ot" => open_timestamp,
91
- "ct" => close_timestamp,
92
- "iv" => interval.to_s,
93
-
94
- "o" => open_price,
95
- "h" => high_price,
96
- "l" => low_price,
97
- "c" => close_price,
98
-
99
- "bv" => base_volume,
100
- "tv" => target_volume,
101
-
102
- "t" => trades,
103
- "g" => green,
104
- "j" => doji }
105
- end
106
-
107
- def as_price(value)
108
- series.nil? ? value : series.as_price(value)
109
- end
110
-
111
- def to_s
112
- ots = interval.daily? ? open_timestamp.strftime('%Y-%m-%d') : open_timestamp.strftime('%Y-%m-%d %H:%M:%S')
113
- cts = interval.daily? ? close_timestamp.strftime('%Y-%m-%d') : close_timestamp.strftime('%Y-%m-%d %H:%M:%S')
114
- "#{ots}: o: #{as_price(open_price)}, h: #{as_price(high_price)}, l: #{as_price(low_price)}, c: #{as_price(close_price)} :#{cts}"
80
+ # Calculates the absolute change from the open_price to the close_price, divided by the average of the
81
+ # open_price and close_price. This method will give a value between 0 and 2, where 0 means no change,
82
+ # 1 means the price doubled, and 2 means the price went to zero.
83
+ # This method is useful for comparing the volatility of different assets.
84
+ # @return [Float]
85
+ def daily_price_change_ratio
86
+ @price_change ||= ((open_price - close_price) / oc2).abs
115
87
  end
116
88
 
89
+ # Set the #green? property to true when the close_price is greater than or equal to the open_price.
117
90
  def compute_green
118
91
  close_price >= open_price
119
92
  end
120
93
 
121
94
  def green?
122
- close_price > open_price
95
+ @green
123
96
  end
124
97
 
125
98
  def red?
@@ -130,10 +103,10 @@ module Quant
130
103
  @doji
131
104
  end
132
105
 
133
- def price_change
134
- @price_change ||= ((open_price - close_price) / oc2).abs
135
- end
136
-
106
+ # Computes a doji candlestick pattern. A doji is a candlestick pattern that occurs when the open and close
107
+ # are the same or very close to the same. The high and low are also very close to the same. The doji pattern
108
+ # is a sign of indecision in the market. It is a sign that the market is not sure which way to go.
109
+ # @return [Boolean]
137
110
  def compute_doji
138
111
  body_bottom, body_top = [open_price, close_price].sort
139
112
 
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Ticks
5
+ module Serializers
6
+ class OHLC < Tick
7
+ # Returns a +Quant::Ticks::Tick+ from a valid JSON +String+.
8
+ # @param json [String]
9
+ # @param tick_class [Quant::Ticks::Tick]
10
+ # @return [Quant::Ticks::Tick]
11
+ # @example
12
+ # json =
13
+ # Quant::Ticks::Serializers::Tick.from_json(json, tick_class: Quant::Ticks::Spot)
14
+ def self.from_json(json, tick_class:)
15
+ hash = Oj.load(json)
16
+ from(hash, tick_class: tick_class)
17
+ end
18
+
19
+ # Returns a +Hash+ of the Spot tick's key properties
20
+ #
21
+ # Serialized Keys:
22
+ #
23
+ # - ot: open timestamp
24
+ # - ct: close timestamp
25
+ # - iv: interval
26
+ # - o: open price
27
+ # - h: high price
28
+ # - l: low price
29
+ # - c: close price
30
+ # - bv: base volume
31
+ # - tv: target volume
32
+ # - t: trades
33
+ # - g: green
34
+ # - j: doji
35
+ #
36
+ # @param tick [Quant::Ticks::Tick]
37
+ # @return [Hash]
38
+ # @example
39
+ # Quant::Ticks::Serializers::Tick.to_h(tick)
40
+ # # => { "ot" => [Time], "ct" => [Time], "iv" => "1m", "o" => 1.0, "h" => 2.0,
41
+ # # "l" => 0.5, "c" => 1.5, "bv" => 6.0, "tv" => 5.0, "t" => 1, "g" => true, "j" => true }
42
+ def self.to_h(tick)
43
+ { "ot" => tick.open_timestamp,
44
+ "ct" => tick.close_timestamp,
45
+ "iv" => tick.interval.to_s,
46
+
47
+ "o" => tick.open_price,
48
+ "h" => tick.high_price,
49
+ "l" => tick.low_price,
50
+ "c" => tick.close_price,
51
+
52
+ "bv" => tick.base_volume,
53
+ "tv" => tick.target_volume,
54
+
55
+ "t" => tick.trades,
56
+ "g" => tick.green,
57
+ "j" => tick.doji }
58
+ end
59
+
60
+ def self.from(hash, tick_class:)
61
+ tick_class.new \
62
+ open_timestamp: hash["ot"],
63
+ close_timestamp: hash["ct"],
64
+ interval: hash["iv"],
65
+
66
+ open_price: hash["o"],
67
+ high_price: hash["h"],
68
+ low_price: hash["l"],
69
+ close_price: hash["c"],
70
+
71
+ base_volume: hash["bv"],
72
+ target_volume: hash["tv"],
73
+
74
+ trades: hash["t"],
75
+ green: hash["g"],
76
+ doji: hash["j"]
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Ticks
5
+ module Serializers
6
+ class Spot < Tick
7
+ # Returns a +Quant::Ticks::Tick+ from a valid JSON +String+.
8
+ # @param json [String]
9
+ # @param tick_class [Quant::Ticks::Tick]
10
+ # @return [Quant::Ticks::Tick]
11
+ # @example
12
+ # json = "{\"ct\":\"2024-01-15 03:12:23 UTC\", \"cp\":5.0, \"iv\":\"1d\", \"bv\":0.0, \"tv\":0.0, \"t\":0}"
13
+ # Quant::Ticks::Serializers::Tick.from_json(json, tick_class: Quant::Ticks::Spot)
14
+ def self.from_json(json, tick_class:)
15
+ hash = Oj.load(json)
16
+ from(hash, tick_class: tick_class)
17
+ end
18
+
19
+ # Returns a +Hash+ of the Spot tick's key properties
20
+ #
21
+ # Serialized Keys:
22
+ #
23
+ # - ct: close timestamp
24
+ # - iv: interval
25
+ # - cp: close price
26
+ # - bv: base volume
27
+ # - tv: target volume
28
+ # - t: trades
29
+ #
30
+ # @param tick [Quant::Ticks::Tick]
31
+ # @return [Hash]
32
+ # @example
33
+ # Quant::Ticks::Serializers::Tick.to_h(tick)
34
+ # # => { "ct" => "2024-02-13 03:12:23 UTC", "cp" => 5.0, "iv" => "1d", "bv" => 0.0, "tv" => 0.0, "t" => 0 }
35
+ def self.to_h(tick)
36
+ { "ct" => tick.close_timestamp,
37
+ "cp" => tick.close_price,
38
+ "iv" => tick.interval.to_s,
39
+ "bv" => tick.base_volume,
40
+ "tv" => tick.target_volume,
41
+ "t" => tick.trades }
42
+ end
43
+
44
+ def self.from(hash, tick_class:)
45
+ tick_class.new(
46
+ close_timestamp: hash["ct"],
47
+ close_price: hash["cp"],
48
+ interval: hash["iv"],
49
+ base_volume: hash["bv"],
50
+ target_volume: hash["tv"],
51
+ trades: hash["t"]
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Ticks
5
+ module Serializers
6
+ # The +Tick+ class serializes and deserializes +Tick+ objects to and from various formats.
7
+ # These classes are wired into the Tick classes and are used to convert +Tick+ objects to and from
8
+ # Ruby hashes, JSON strings, and CSV strings. They're not typically used directly, but are extracted
9
+ # to make it easier to provide custom serialization and deserialization for +Tick+ objects not shipped
10
+ # with the library.
11
+ # @abstract
12
+ class Tick
13
+ # Returns a +String+ that is a valid CSV row
14
+ # @param tick [Quant::Ticks::Tick]
15
+ # @param headers [Boolean]
16
+ # @return [String]
17
+ # @example
18
+ # Quant::Ticks::Serializers::Tick.to_csv(tick)
19
+ # # => "1d,9999,5.0\n"
20
+ # @example
21
+ # Quant::Ticks::Serializers::Tick.to_csv(tick, headers: true)
22
+ # # => "iv,ct,cp\n1d,9999,5.0\n"
23
+ def self.to_csv(tick, headers: false)
24
+ hash = to_h(tick)
25
+ row = CSV::Row.new(hash.keys, hash.values)
26
+ return row.to_csv unless headers
27
+
28
+ header_row = CSV::Row.new(hash.keys, hash.keys)
29
+ header_row.to_csv << row.to_csv
30
+ end
31
+
32
+ # Returns a +String+ that is a valid JSON representation of the tick's key properties.
33
+ # @param tick [Quant::Ticks::Tick]
34
+ # @return [String]
35
+ # @example
36
+ # Quant::Ticks::Serializers::Tick.to_json(tick)
37
+ # # => "{\"iv\":\"1d\",\"ct\":9999,\"cp\":5.0}"
38
+ def self.to_json(tick)
39
+ Oj.dump to_h(tick)
40
+ end
41
+
42
+ # Returns a Ruby +Hash+ comprised of the tick's key properties.
43
+ # @param tick [Quant::Ticks::Tick]
44
+ # @return [Hash]
45
+ # @example
46
+ # Quant::Ticks::Serializers::Tick.to_h(tick)
47
+ # # => { "iv" => "1d", "ct" => 9999, "cp" => 5.0 }
48
+ def self.to_h(instance)
49
+ raise NotImplementedError
50
+ end
51
+
52
+ # Returns a +Quant::Ticks::Tick+ from a Ruby +Hash+.
53
+ # @param hash [Hash]
54
+ # @param tick_class [Quant::Ticks::Tick]
55
+ # @return [Quant::Ticks::Tick]
56
+ # @example
57
+ # hash = { "ct" => 2024-01-15 03:12:23 UTC", "cp" => 5.0, "iv" => "1d", "bv" => 0.0, "tv" => 0.0, "t" => 0}
58
+ # Quant::Ticks::Serializers::Tick.from(hash, tick_class: Quant::Ticks::Spot)
59
+ def self.from(hash, tick_class:)
60
+ raise NotImplementedError
61
+ end
62
+
63
+ # Returns a +Quant::Ticks::Tick+ from a valid JSON +String+.
64
+ # @param json [String]
65
+ # @param tick_class [Quant::Ticks::Tick]
66
+ # @return [Quant::Ticks::Tick]
67
+ # @example
68
+ # json = "{\"ct\":\"2024-01-15 03:12:23 UTC\", \"cp\":5.0, \"iv\":\"1d\", \"bv\":0.0, \"tv\":0.0, \"t\":0}"
69
+ # Quant::Ticks::Serializers::Tick.from_json(json, tick_class: Quant::Ticks::Spot)
70
+ def self.from_json(json, tick_class:)
71
+ hash = Oj.load(json)
72
+ from(hash, tick_class: tick_class)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -1,32 +1,78 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "tick"
4
+
3
5
  module Quant
4
6
  module Ticks
5
- class Spot < Value
6
- def self.from(hash)
7
- new(close_timestamp: hash["ct"], close_price: hash["c"], base_volume: hash["bv"], target_volume: hash["tv"])
8
- end
7
+ # A +Spot+ is a single price point in time. It is the most basic form of a +Tick+ and is usually used to represent
8
+ # a continuously streaming tick that just has a single price point at a given point in time.
9
+ # @example
10
+ # spot = Quant::Ticks::Spot.new(price: 100.0, timestamp: Time.now)
11
+ # spot.price # => 100.0
12
+ # spot.timestamp # => 2018-01-01 12:00:00 UTC
13
+ #
14
+ # @example
15
+ # spot = Quant::Ticks::Spot.from({ "p" => 100.0, "t" => "2018-01-01 12:00:00 UTC", "bv" => 1000 })
16
+ # spot.price # => 100.0
17
+ # spot.timestamp # => 2018-01-01 12:00:00 UTC
18
+ # spot.volume # => 1000
19
+ class Spot < Tick
20
+ include TimeMethods
21
+
22
+ attr_reader :interval, :series
23
+ attr_reader :close_timestamp, :open_timestamp
24
+ attr_reader :open_price, :high_price, :low_price, :close_price
25
+ attr_reader :base_volume, :target_volume, :trades
26
+
27
+ def initialize(
28
+ price: nil,
29
+ timestamp: nil,
30
+ close_price: nil,
31
+ close_timestamp: nil,
32
+ volume: nil,
33
+ interval: nil,
34
+ base_volume: nil,
35
+ target_volume: nil,
36
+ trades: nil
37
+ )
38
+ raise ArgumentError, "Must supply a spot price as either :price or :close_price" unless price || close_price
39
+
40
+ @close_price = (close_price || price).to_f
9
41
 
10
- def self.from_json(json)
11
- from Oj.load(json)
42
+ @interval = Interval[interval]
43
+
44
+ @close_timestamp = extract_time(timestamp || close_timestamp || Quant.current_time)
45
+ @open_timestamp = @close_timestamp
46
+
47
+ @base_volume = (volume || base_volume).to_i
48
+ @target_volume = (target_volume || @base_volume).to_i
49
+
50
+ @trades = trades.to_i
51
+ super()
12
52
  end
13
53
 
14
- def initialize(close_timestamp:, close_price:, interval: nil, base_volume: 0.0, target_volume: 0.0, trades: 0)
15
- super(price: close_price, timestamp: close_timestamp, interval: interval, volume: base_volume, trades: trades)
16
- @target_volume = target_volume.to_i
54
+ alias timestamp close_timestamp
55
+ alias price close_price
56
+ alias oc2 close_price
57
+ alias hl2 close_price
58
+ alias hlc3 close_price
59
+ alias ohlc4 close_price
60
+ alias delta close_price
61
+ alias volume base_volume
62
+
63
+ def ==(other)
64
+ [close_price, close_timestamp] == [other.close_price, other.close_timestamp]
17
65
  end
18
66
 
67
+ # The corresponding? method helps determine that the other tick's timestamp is the same as this tick's timestamp,
68
+ # which is useful when aligning ticks between two separate series where one starts or ends at a different time,
69
+ # or when there may be gaps in the data between the two series.
19
70
  def corresponding?(other)
20
71
  close_timestamp == other.close_timestamp
21
72
  end
22
73
 
23
- def to_h
24
- { "ct" => close_timestamp,
25
- "c" => close_price,
26
- "iv" => interval.to_s,
27
- "bv" => base_volume,
28
- "tv" => target_volume,
29
- "t" => trades }
74
+ def inspect
75
+ "#<#{self.class.name} cp=#{close_price.to_f} ct=#{close_timestamp.strftime("%Y-%m-%d")} iv=#{interval.to_s} v=#{volume}>"
30
76
  end
31
77
  end
32
78
  end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Ticks
5
+ # +Tick+ is the abstract ancestor for all Ticks and holds the logic for interacting with series and indicators.
6
+ # The public interface is devoid of properties around price, volume, and timestamp, etc. Descendant classes
7
+ # are responsible for defining the properties and how they are represented.
8
+ #
9
+ # The +Tick+ class is designed to be immutable and is intended to be used as a value object. This means that
10
+ # once a +Tick+ is created, it cannot be changed. This is important for the integrity of the series and
11
+ # indicators that depend on the ticks within the series.
12
+ #
13
+ # When a tick is added to a series, it is locked into the series and ownership cannot be changed. This is important
14
+ # for the integrity of the series and indicators that depend on the ticks within the series. This is a key design
15
+ # to being able to being able to not only compute indicators on the ticks just once, but also avoid recomputing
16
+ # indicators when series are limited/sliced/filtered into subsets of the original series.
17
+ #
18
+ # Ticks can be serialized to and from Ruby Hash, JSON strings, and CSV strings.
19
+ class Tick
20
+ # Returns a +Tick+ from a Ruby +Hash+. The default serializer is used to generate the +Tick+.
21
+ # @param hash [Hash]
22
+ # @param serializer_class [Class] The serializer class to use for the conversion.
23
+ # @return [Quant::Ticks::Tick]
24
+ # @example
25
+ # hash = { "timestamp" => "2018-01-01 12:00:00 UTC", "price" => 100.0, "volume" => 1000 }
26
+ # Quant::Ticks::Tick.from(hash)
27
+ # # => #<Quant::Ticks::Spot:0x00007f9e3b8b3e08 @timestamp=2018-01-01 12:00:00 UTC, @price=100.0, @volume=1000>
28
+ def self.from(hash, serializer_class: default_serializer_class)
29
+ serializer_class.from(hash, tick_class: self)
30
+ end
31
+
32
+ # Returns a +Tick+ from a JSON string. The default serializer is used to generate the +Tick+.
33
+ # @param json [String]
34
+ # @param serializer_class [Class] The serializer class to use for the conversion.
35
+ # @return [Quant::Ticks::Tick]
36
+ # @example
37
+ # json = "{\"timestamp\":\"2018-01-01 12:00:00 UTC\",\"price\":100.0,\"volume\":1000}"
38
+ # Quant::Ticks::Tick.from_json(json)
39
+ # # => #<Quant::Ticks::Spot:0x00007f9e3b8b3e08 @timestamp=2018-01-01 12:00:00 UTC, @price=100.0, @volume=1000>
40
+ def self.from_json(json, serializer_class: default_serializer_class)
41
+ serializer_class.from_json(json, tick_class: self)
42
+ end
43
+
44
+ attr_reader :series
45
+
46
+ def initialize
47
+ # Set the series by appending to the series or calling #assign_series method
48
+ @series = nil
49
+ end
50
+
51
+ # Ticks always belong to the first series they're assigned so we can easily spin off
52
+ # sub-sets or new series with the same ticks while allowing each series to have
53
+ # its own state and full control over the ticks within its series
54
+ def assign_series(new_series)
55
+ assign_series!(new_series) if @series.nil?
56
+ self
57
+ end
58
+
59
+ # Ticks always belong to the first series they're assigned so we can easily spin off
60
+ # sub-sets or new series with the same ticks. However, if you need to reassign the
61
+ # series, you can use this method to force the change of series ownership.
62
+ #
63
+ # The series interval is also assigned to the tick if it is not already set.
64
+ def assign_series!(new_series)
65
+ @series = new_series
66
+ @interval = new_series.interval if @interval.nil?
67
+ self
68
+ end
69
+
70
+ # Returns a Ruby hash for the Tick. The default serializer is used to generate the hash.
71
+ #
72
+ # @param serializer_class [Class] the serializer class to use for the conversion.
73
+ # @example
74
+ # tick.to_h
75
+ # # => { timestamp: "2018-01-01 12:00:00 UTC", price: 100.0, volume: 1000 }
76
+ def to_h(serializer_class: default_serializer_class)
77
+ serializer_class.to_h(self)
78
+ end
79
+
80
+ # Returns a JSON string for the Tick. The default serializer is used to generate the JSON string.
81
+ #
82
+ # @param serializer_class [Class] the serializer class to use for the conversion.
83
+ # @example
84
+ # tick.to_json
85
+ # # => "{\"timestamp\":\"2018-01-01 12:00:00 UTC\",\"price\":100.0,\"volume\":1000}"
86
+ def to_json(serializer_class: default_serializer_class)
87
+ serializer_class.to_json(self)
88
+ end
89
+
90
+ # Returns a CSV row as a String for the Tick. The default serializer is used to generate the CSV string.
91
+ # If headers is true, two lines returned separated by newline.
92
+ # The first line is the header row and the second line is the data row.
93
+ #
94
+ # @param serializer_class [Class] the serializer class to use for the conversion.
95
+ # @example
96
+ # tick.to_csv(headers: true)
97
+ # # => "timestamp,price,volume\n2018-01-01 12:00:00 UTC,100.0,1000\n"
98
+ def to_csv(serializer_class: default_serializer_class, headers: false)
99
+ serializer_class.to_csv(self, headers: headers)
100
+ end
101
+
102
+ # Reflects the serializer class from the tick's class name.
103
+ # @note internal use only.
104
+ def self.default_serializer_class
105
+ Object.const_get "Quant::Ticks::Serializers::#{name.split("::").last}"
106
+ end
107
+
108
+ # Reflects the serializer class from the tick's class name.
109
+ # @note internal use only.
110
+ def default_serializer_class
111
+ self.class.default_serializer_class
112
+ end
113
+ end
114
+ end
115
+ end
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.0"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/quantitative.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "time"
4
4
  require "date"
5
5
  require "oj"
6
+ require "csv"
6
7
 
7
8
  lib_folder = File.expand_path(File.join(File.dirname(__FILE__)))
8
9
  quant_folder = File.join(lib_folder, "quant")
@@ -11,7 +12,6 @@ quant_folder = File.join(lib_folder, "quant")
11
12
  Dir.glob(File.join(quant_folder, "*.rb")).each { |fn| require fn }
12
13
 
13
14
  # require sub-folders and their sub-folders
14
- %w(ticks).each do |sub_folder|
15
- Dir.glob(File.join(quant_folder, sub_folder, "*.rb")).each { |fn| require fn }
15
+ %w(refinements ticks).each do |sub_folder|
16
16
  Dir.glob(File.join(quant_folder, sub_folder, "**/*.rb")).each { |fn| require fn }
17
17
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quantitative
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Lang
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-11 00:00:00.000000000 Z
11
+ date: 2024-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -24,62 +24,6 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '3.10'
27
- - !ruby/object:Gem::Dependency
28
- name: debug
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: guard-rspec
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '4.7'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '4.7'
55
- - !ruby/object:Gem::Dependency
56
- name: rspec
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '3.2'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '3.2'
69
- - !ruby/object:Gem::Dependency
70
- name: yard
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '0.9'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '0.9'
83
27
  description: Quantitative and statistical tools written for Ruby 3.x for trading and
84
28
  finance.
85
29
  email:
@@ -90,12 +34,14 @@ extra_rdoc_files: []
90
34
  files:
91
35
  - ".rspec"
92
36
  - ".rubocop.yml"
37
+ - ".ruby-version"
38
+ - ".yardopts"
93
39
  - CODE_OF_CONDUCT.md
94
- - DISCLAIMER.txt
40
+ - DISCLAIMER.md
95
41
  - Gemfile
96
42
  - Gemfile.lock
97
43
  - Guardfile
98
- - LICENSE.txt
44
+ - LICENSE
99
45
  - README.md
100
46
  - Rakefile
101
47
  - lib/quant/errors.rb
@@ -109,12 +55,14 @@ files:
109
55
  - lib/quant/mixins/super_smoother.rb
110
56
  - lib/quant/mixins/trig.rb
111
57
  - lib/quant/mixins/weighted_average.rb
58
+ - lib/quant/refinements/array.rb
112
59
  - lib/quant/ticks/core.rb
113
- - lib/quant/ticks/indicator_points.rb
114
60
  - lib/quant/ticks/ohlc.rb
115
- - lib/quant/ticks/serializers/value.rb
61
+ - lib/quant/ticks/serializers/ohlc.rb
62
+ - lib/quant/ticks/serializers/spot.rb
63
+ - lib/quant/ticks/serializers/tick.rb
116
64
  - lib/quant/ticks/spot.rb
117
- - lib/quant/ticks/value.rb
65
+ - lib/quant/ticks/tick.rb
118
66
  - lib/quant/time_methods.rb
119
67
  - lib/quant/time_period.rb
120
68
  - lib/quant/version.rb
@@ -142,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
90
  - !ruby/object:Gem::Version
143
91
  version: '0'
144
92
  requirements: []
145
- rubygems_version: 3.3.7
93
+ rubygems_version: 3.5.5
146
94
  signing_key:
147
95
  specification_version: 4
148
96
  summary: Quantitative and statistical tools written for Ruby 3.x for trading and finance.
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Quant
4
- module Ticks
5
- class IndicatorPoints
6
- attr_reader :points
7
-
8
- def initialize(tick:)
9
- @tick = tick
10
- @points = {}
11
- end
12
-
13
- def [](indicator)
14
- points[indicator]
15
- end
16
-
17
- def []=(indicator, point)
18
- points[indicator] = point
19
- end
20
- end
21
- end
22
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Quant
4
- module Ticks
5
- module Serializers
6
- module Value
7
- module_function
8
-
9
- def to_h(instance)
10
- { "iv" => instance.interval.to_s,
11
- "ct" => instance.close_timestamp.to_i,
12
- "cp" => instance.close_price,
13
- "bv" => instance.base_volume,
14
- "tv" => instance.target_volume }
15
- end
16
-
17
- def to_json(instance)
18
- Oj.dump to_h(instance)
19
- end
20
- end
21
- end
22
- end
23
- end
@@ -1,88 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Quant
4
- module Ticks
5
- # Value ticks are the most basic ticks and are used to represent a single price point (no open, high, low, close, etc.)
6
- # and a single timestamp. Usually, these are best used in streaming data where ticks are flowing in every second or whatever
7
- # interval that's appropriate for the data source.
8
- # Often indicators and charts still want a universal public interface (i.e. open_price, high_price, volume, etc.), so we add
9
- # those methods here and inherit and redefine upstream as appropriate.
10
- #
11
- # For Value ticks:
12
- # * The +price+ given is set for all *_price fields.
13
- # * The +volume+ is set for both base and target volume.
14
- # * The +timestamp+ is set for both open and close timestamps.
15
- class Value
16
- include TimeMethods
17
-
18
- attr_reader :interval, :series
19
- attr_reader :close_timestamp, :open_timestamp
20
- attr_reader :open_price, :high_price, :low_price, :close_price
21
- attr_reader :base_volume, :target_volume, :trades
22
- attr_reader :green, :doji
23
-
24
- def initialize(price:, timestamp: Quant.current_time, interval: nil, volume: 0, trades: 0)
25
- @interval = Interval[interval]
26
-
27
- @close_timestamp = extract_time(timestamp)
28
- @open_timestamp = @close_timestamp
29
-
30
- @close_price = price.to_f
31
- @open_price = close_price
32
- @high_price = close_price
33
- @low_price = close_price
34
-
35
- @base_volume = volume.to_i
36
- @target_volume = volume.to_i
37
- @trades = trades.to_i
38
-
39
- # Set the series by appending to the series
40
- @series = nil
41
- end
42
-
43
- alias oc2 close_price
44
- alias hl2 close_price
45
- alias hlc3 close_price
46
- alias ohlc4 close_price
47
- alias delta close_price
48
- alias volume base_volume
49
-
50
- def corresponding?(other)
51
- close_timestamp == other.close_timestamp
52
- end
53
-
54
- def ==(other)
55
- to_h == other.to_h
56
- end
57
-
58
- # ticks are immutable across series so we can easily initialize sub-sets or new series
59
- # with the same ticks while allowing each series to have its own state and full
60
- # control over the ticks within its series
61
- def assign_series(new_series)
62
- assign_series!(new_series) if @series.nil?
63
-
64
- # dup.tap do |new_tick|
65
- # # new_tick.instance_variable_set(:@series, new_series)
66
- # new_tick.instance_variable_set(:@indicators, indicators)
67
- # end
68
- end
69
-
70
- def assign_series!(new_series)
71
- @series = new_series
72
- self
73
- end
74
-
75
- def inspect
76
- "#<#{self.class.name} iv=#{interval} ct=#{close_timestamp.strftime("%Y-%m-%d")} o=#{open_price} c=#{close_price} v=#{volume}>"
77
- end
78
-
79
- def to_h
80
- Quant::Ticks::Serializers::Value.to_h(self)
81
- end
82
-
83
- def to_json(*_args)
84
- Quant::Ticks::Serializers::Value.to_json(self)
85
- end
86
- end
87
- end
88
- end
File without changes
File without changes