quantitative 0.1.1 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b99f939e67992ff9a3659ef69ef8ff4bd5897baba1d269c491a7db7d1717c85
4
- data.tar.gz: e9f456c34e16ed0c44b4b23f6e274ed44cce4ca6ad7fe654602dbd73afc281bc
3
+ metadata.gz: 5bfe61d98618c7cfb83052426689976be38c45e83fce9217a74a4f0392897b60
4
+ data.tar.gz: 061d9a0179eb913c5f5f5d9f1016998c2cbc0d65da9a774620a3b591f18a1f49
5
5
  SHA512:
6
- metadata.gz: 4fca7722561f54543b92a7b50700eed4cb706b9bfca65405a8a101908b6ae49b547bcdd2cd13c4656c94a30ad817225118c22c905e47076c88f8feff85947bc4
7
- data.tar.gz: 78b256560d7aec2ee19833618bd19e89f6c87ae718ad966b1d18caa18e08727b37d1232b169bce1fbc0b4ca4bac951e956fd4efefaa27450d533769f68d8ac23
6
+ metadata.gz: 4154a9e589bcffd15b3415ace9c1ccc6b8d29ba5ecc0180e48259c649a746a157f58bd89aeabad4ffc4ac5edefe6f1892c94adcd001ba5d1f039a5bb46a8a9d0
7
+ data.tar.gz: 7ac2b4661e6c4b0e02fda9389f73b8d007b92e754e8b4fa821bb55dca1d085dfd7e2e3eb6608d520a2e2e6008eb837aa75e7c07c05addc28af577b92fe98f8c3
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quantitative (0.1.1)
4
+ quantitative (0.1.3)
5
5
  oj (~> 3.10)
6
6
 
7
7
  GEM
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
@@ -5,4 +5,5 @@ module Quant
5
5
  class InvalidInterval < Error; end
6
6
  class InvalidResolution < Error; end
7
7
  class ArrayMaxSizeError < Error; end
8
+ class SecurityClassError < Error; end
8
9
  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
@@ -100,7 +100,7 @@ module Quant
100
100
  end
101
101
 
102
102
  # Computes the mean of the array. When +n+ is specified, the mean is computed over
103
- # the last +n+ elements.
103
+ # the last +n+ elements, otherwise it is computed over the entire array.
104
104
  #
105
105
  # @param n [Integer] the number of elements to compute the mean over
106
106
  # @return [Float]
@@ -112,7 +112,7 @@ module Quant
112
112
  end
113
113
 
114
114
  # Computes the Exponential Moving Average (EMA) of the array. When +n+ is specified,
115
- # the EMA is computed over the last +n+ elements.
115
+ # the EMA is computed over the last +n+ elements, otherwise it is computed over the entire array.
116
116
  # An Array of EMA's is returned, with the first entry always the first value in the subset.
117
117
  #
118
118
  # @params n [Integer] the number of elements to compute the EMA over.
@@ -130,7 +130,7 @@ module Quant
130
130
  end
131
131
 
132
132
  # Computes the Simple Moving Average (SMA) of the array. When +n+ is specified,
133
- # the SMA is computed over the last +n+ elements.
133
+ # the SMA is computed over the last +n+ elements, otherwise it is computed over the entire array.
134
134
  # An Array of SMA's is returned, with the first entry always the first value in the subset.
135
135
  #
136
136
  # @param n [Integer] the number of elements to compute the SMA over
@@ -146,7 +146,7 @@ module Quant
146
146
  end
147
147
 
148
148
  # Computes the Weighted Moving Average (WMA) of the array. When +n+ is specified,
149
- # the WMA is computed over the last +n+ elements.
149
+ # the WMA is computed over the last +n+ elements, otherwise it is computed over the entire array.
150
150
  # An Array of WMA's is returned, with the first entry always the first value in the subset.
151
151
  #
152
152
  # @param n [Integer] the number of elements to compute the WMA over
@@ -168,7 +168,8 @@ module Quant
168
168
  end
169
169
 
170
170
  # Computes the Standard Deviation of the array. When +n+ is specified,
171
- # the Standard Deviation is computed over the last +n+ elements.
171
+ # the Standard Deviation is computed over the last +n+ elements,
172
+ # otherwise it is computed over the entire array.
172
173
  #
173
174
  # @param n [Integer] the number of elements to compute the Standard Deviation over.
174
175
  # @return [Float]
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "security_class"
4
+
5
+ module Quant
6
+ # A +Security+ is a representation of a financial instrument such as a stock, option, future, or currency.
7
+ # It is used to represent the instrument that is being traded, analyzed, or managed.
8
+ # @example
9
+ # security = Quant::Security.new(symbol: "AAPL", name: "Apple Inc.", security_class: :stock, exchange: "NASDAQ")
10
+ # security.symbol # => "AAPL"
11
+ # security.name # => "Apple Inc."
12
+ # security.stock? # => true
13
+ # security.option? # => false
14
+ # security.future? # => false
15
+ # security.currency? # => false
16
+ # security.exchange # => "NASDAQ"
17
+ # security.to_h # => { "s" => "AAPL", "n" => "Apple Inc.", "sc" => "stock", "x" => "NASDAQ" }
18
+ class Security
19
+ attr_reader :symbol, :name, :security_class, :id, :exchange, :source, :meta, :created_at, :updated_at
20
+
21
+ def initialize(
22
+ symbol:,
23
+ name: nil,
24
+ id: nil,
25
+ active: true,
26
+ tradeable: true,
27
+ exchange: nil,
28
+ source: nil,
29
+ security_class: nil,
30
+ created_at: Quant.current_time,
31
+ updated_at: Quant.current_time,
32
+ meta: {}
33
+ )
34
+ raise ArgumentError, "symbol is required" unless symbol
35
+
36
+ @symbol = symbol.to_s.upcase
37
+ @name = name
38
+ @id = id
39
+ @tradeable = tradeable
40
+ @active = active
41
+ @exchange = exchange
42
+ @source = source
43
+ @security_class = SecurityClass.new(security_class)
44
+ @created_at = created_at
45
+ @updated_at = updated_at
46
+ @meta = meta
47
+ end
48
+
49
+ def active?
50
+ !!@active
51
+ end
52
+
53
+ def tradeable?
54
+ !!@tradeable
55
+ end
56
+
57
+ SecurityClass::CLASSES.each do |class_name|
58
+ define_method("#{class_name}?") do
59
+ security_class == class_name
60
+ end
61
+ end
62
+
63
+ def to_h(full: false)
64
+ return { "s" => symbol } unless full
65
+
66
+ { "s" => symbol,
67
+ "n" => name,
68
+ "id" => id,
69
+ "t" => tradeable?,
70
+ "a" => active?,
71
+ "x" => exchange,
72
+ "sc" => security_class.to_s,
73
+ "src" => source.to_s }
74
+ end
75
+
76
+ def to_json(*args, full: false)
77
+ Oj.dump(to_h(full: full), *args)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ # Stocks (Equities): Represent ownership in a company. Stockholders are entitled to a share
5
+ # of the company's profits and have voting rights at shareholder meetings.
6
+
7
+ # Bonds (Fixed-Income Securities): Debt instruments where an investor lends money to an entity
8
+ # (government or corporation) in exchange for periodic interest payments and the return of
9
+ # the principal amount at maturity.
10
+
11
+ # Mutual Funds: Pooled funds managed by an investment company. Investors buy shares in the mutual
12
+ # fund, and the fund invests in a diversified portfolio of stocks, bonds, or other securities.
13
+
14
+ # Exchange-Traded Funds (ETFs): Similar to mutual funds but traded on stock exchanges.
15
+ # ETFs can track an index, commodity, bonds, or a basket of assets.
16
+
17
+ # Options: Derivative securities that give the holder the right (but not the obligation) to buy
18
+ # or sell an underlying asset at a predetermined price before or at expiration.
19
+
20
+ # Futures: Contracts that obligate the buyer to purchase or the seller to sell an
21
+ # asset at a predetermined future date and price.
22
+
23
+ # Real Estate Investment Trusts (REITs): Companies that own, operate, or finance income-generating
24
+ # real estate. Investors can buy shares in a REIT, which provides them with a share of the
25
+ # income produced by the real estate.
26
+
27
+ # Cryptocurrencies: Digital or virtual currencies that use cryptography for security and operate on
28
+ # decentralized networks, typically based on blockchain technology. Examples include Bitcoin, Ethereum, and Ripple.
29
+
30
+ # Preferred Stock: A type of stock that has priority over common stock in terms of dividend
31
+ # payments and asset distribution in the event of liquidation.
32
+
33
+ # Treasury Securities: Issued by the government to raise funds. Types include Treasury bills (T-bills),
34
+ # Treasury notes (T-notes), and Treasury bonds (T-bonds).
35
+
36
+ # Mortgage-Backed Securities (MBS): Securities that represent an ownership interest in a pool of mortgage loans.
37
+ # Investors receive payments based on the interest and principal of the underlying loans.
38
+
39
+ # Commodities: Physical goods such as gold, silver, oil, or agricultural products, traded on commodity exchanges.
40
+
41
+ # Foreign Exchange (Forex): The market where currencies are traded. Investors can buy and sell currencies to
42
+ # profit from changes in exchange rates.
43
+ class SecurityClass
44
+ CLASSES = %i(
45
+ bond
46
+ commodity
47
+ cryptocurrency
48
+ etf
49
+ forex
50
+ future
51
+ mbs
52
+ mutual_fund
53
+ option
54
+ preferred_stock
55
+ reit
56
+ stock
57
+ treasury_note
58
+ ).freeze
59
+
60
+ attr_reader :security_class
61
+
62
+ def initialize(name)
63
+ return if @security_class = from_standard(name)
64
+
65
+ @security_class = from_alternate(name.to_s.downcase.to_sym) unless name.nil?
66
+ raise_unknown_security_class_error(name) unless security_class
67
+ end
68
+
69
+ CLASSES.each do |class_name|
70
+ define_method("#{class_name}?") do
71
+ security_class == class_name
72
+ end
73
+ end
74
+
75
+ def raise_unknown_security_class_error(name)
76
+ raise SecurityClassError, "Unknown security class: #{name.inspect}"
77
+ end
78
+
79
+ def to_s
80
+ security_class.to_s
81
+ end
82
+
83
+ def to_h
84
+ { "sc" => security_class }
85
+ end
86
+
87
+ def to_json(*args)
88
+ Oj.dump(to_h, *args)
89
+ end
90
+
91
+ def ==(other)
92
+ case other
93
+ when String then from_alternate(other.to_sym) == security_class
94
+ when Symbol then from_alternate(other) == security_class
95
+ when SecurityClass then other.security_class == security_class
96
+ else
97
+ false
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ ALTERNATE_NAMES = {
104
+ us_equity: :stock,
105
+ crypto: :cryptocurrency,
106
+ }.freeze
107
+
108
+ def from_standard(name)
109
+ name if CLASSES.include?(name)
110
+ end
111
+
112
+ def from_alternate(alt_name)
113
+ from_standard(alt_name) || ALTERNATE_NAMES[alt_name]
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ # Ticks belong to the first series they're associated with always.
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
7
+ # can safely work with subsets of a series and indicators will compute just once.
8
+ class Series
9
+ include Enumerable
10
+ extend Forwardable
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?
17
+ raise "File #{filename} does not exist" unless File.exist?(filename)
18
+
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)
23
+ end
24
+
25
+ def self.from_json(symbol:, interval:, json:)
26
+ from_hash symbol: symbol, interval: interval, hash: Oj.load(json)
27
+ end
28
+
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)
32
+ end
33
+
34
+ def self.from_ticks(symbol:, interval:, ticks:)
35
+ ticks = ticks.sort_by(&:close_timestamp)
36
+
37
+ new(symbol: symbol, interval: interval).tap do |series|
38
+ ticks.each { |tick| series << tick }
39
+ end
40
+ end
41
+
42
+ attr_reader :symbol, :interval, :ticks
43
+
44
+ def initialize(symbol:, interval:)
45
+ @symbol = symbol
46
+ @interval = interval
47
+ @ticks = []
48
+ end
49
+
50
+ def limit_iterations(start_iteration, stop_iteration)
51
+ selected_ticks = ticks[start_iteration..stop_iteration]
52
+ return self if selected_ticks.size == ticks.size
53
+
54
+ self.class.from_ticks(symbol: symbol, interval: interval, ticks: selected_ticks)
55
+ end
56
+
57
+ def limit(period)
58
+ selected_ticks = ticks.select{ |tick| period.cover?(tick.close_timestamp) }
59
+ return self if selected_ticks.size == ticks.size
60
+
61
+ self.class.from_ticks(symbol: symbol, interval: interval, ticks: selected_ticks)
62
+ end
63
+
64
+ def_delegator :@ticks, :[]
65
+ def_delegator :@ticks, :size
66
+ def_delegator :@ticks, :each
67
+ def_delegator :@ticks, :select
68
+ def_delegator :@ticks, :select!
69
+ def_delegator :@ticks, :reject
70
+ def_delegator :@ticks, :reject!
71
+ def_delegator :@ticks, :first
72
+ def_delegator :@ticks, :last
73
+
74
+ def highest
75
+ ticks.max_by(&:high_price)
76
+ end
77
+
78
+ def lowest
79
+ ticks.min_by(&:low_price)
80
+ end
81
+
82
+ def ==(other)
83
+ [symbol, interval, ticks] == [other.symbol, other.interval, other.ticks]
84
+ end
85
+
86
+ def dup
87
+ self.class.from_ticks(symbol: symbol, interval: interval, ticks: ticks)
88
+ end
89
+
90
+ def inspect
91
+ "#<#{self.class.name} symbol=#{symbol} interval=#{interval} ticks=#{ticks.size}>"
92
+ end
93
+
94
+ def <<(tick)
95
+ @ticks << tick.assign_series(self)
96
+ end
97
+
98
+ def to_h
99
+ { "symbol" => symbol,
100
+ "interval" => interval,
101
+ "ticks" => ticks.map(&:to_h) }
102
+ end
103
+
104
+ def to_json(*args)
105
+ Oj.dump(to_h, *args)
106
+ end
107
+ end
108
+ end
@@ -1,125 +1,107 @@
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: interval, trades: trades)
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.
11
+ class OHLC < Tick
12
+ include TimeMethods
13
+
14
+ attr_reader :interval, :series
15
+ attr_reader :close_timestamp, :open_timestamp
16
+ attr_reader :open_price, :high_price, :low_price, :close_price
17
+ attr_reader :base_volume, :target_volume, :trades
18
+ attr_reader :green, :doji
19
+
20
+ def initialize(
21
+ open_timestamp:,
22
+ close_timestamp:,
23
+
24
+ open_price:,
25
+ high_price:,
26
+ low_price:,
27
+ close_price:,
28
+
29
+ interval: nil,
30
+
31
+ volume: nil,
32
+ base_volume: nil,
33
+ target_volume: nil,
34
+
35
+ trades: nil,
36
+ green: nil,
37
+ doji: nil
38
+ )
63
39
  @open_timestamp = extract_time(open_timestamp)
40
+ @close_timestamp = extract_time(close_timestamp)
41
+
64
42
  @open_price = open_price.to_f
65
43
  @high_price = high_price.to_f
66
44
  @low_price = low_price.to_f
45
+ @close_price = close_price.to_f
46
+
47
+ @interval = Interval[interval]
67
48
 
68
- @base_volume = base_volume.to_i
69
- @target_volume = target_volume.to_i
49
+ @base_volume = (volume || base_volume).to_i
50
+ @target_volume = (target_volume || @base_volume).to_i
51
+ @trades = trades.to_i
70
52
 
71
53
  @green = green.nil? ? compute_green : green
72
54
  @doji = doji.nil? ? compute_doji : doji
55
+ super()
73
56
  end
74
57
 
58
+ alias price close_price
59
+ alias timestamp close_timestamp
60
+ alias volume base_volume
61
+
75
62
  def hl2; ((high_price + low_price) / 2.0) end
76
63
  def oc2; ((open_price + close_price) / 2.0) end
77
64
  def hlc3; ((high_price + low_price + close_price) / 3.0) end
78
65
  def ohlc4; ((open_price + high_price + low_price + close_price) / 4.0) end
79
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.
80
70
  def corresponding?(other)
81
71
  [open_timestamp, close_timestamp] == [other.open_timestamp, other.close_timestamp]
82
72
  end
83
73
 
84
- # percent change from open to close
85
- def delta
86
- ((open_price / close_price) - 1.0) * 100
74
+ # Two OHLC ticks are equal if their interval, close_timestamp, and close_price are equal.
75
+ def ==(other)
76
+ [interval, close_timestamp, close_price] == [other.interval, other.close_timestamp, other.close_price]
87
77
  end
88
78
 
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 }
79
+ # Returns the percent daily price change from open_price to close_price, ranging from 0.0 to 1.0.
80
+ # A positive value means the price increased, and a negative value means the price decreased.
81
+ # A value of 0.0 means no change.
82
+ # @return [Float]
83
+ def daily_price_change
84
+ ((open_price / close_price) - 1.0)
85
+ rescue ZeroDivisionError
86
+ 0.0
105
87
  end
106
88
 
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}"
89
+ # Calculates the absolute change from the open_price to the close_price, divided by the average of the
90
+ # open_price and close_price. This method will give a value between 0 and 2, where 0 means no change,
91
+ # 1 means the price doubled, and 2 means the price went to zero.
92
+ # This method is useful for comparing the volatility of different assets.
93
+ # @return [Float]
94
+ def daily_price_change_ratio
95
+ @price_change ||= ((open_price - close_price) / oc2).abs
115
96
  end
116
97
 
98
+ # Set the #green? property to true when the close_price is greater than or equal to the open_price.
117
99
  def compute_green
118
100
  close_price >= open_price
119
101
  end
120
102
 
121
103
  def green?
122
- close_price > open_price
104
+ @green
123
105
  end
124
106
 
125
107
  def red?
@@ -130,10 +112,10 @@ module Quant
130
112
  @doji
131
113
  end
132
114
 
133
- def price_change
134
- @price_change ||= ((open_price - close_price) / oc2).abs
135
- end
136
-
115
+ # Computes a doji candlestick pattern. A doji is a candlestick pattern that occurs when the open and close
116
+ # are the same or very close to the same. The high and low are also very close to the same. The doji pattern
117
+ # is a sign of indecision in the market. It is a sign that the market is not sure which way to go.
118
+ # @return [Boolean]
137
119
  def compute_doji
138
120
  body_bottom, body_top = [open_price, close_price].sort
139
121
 
@@ -147,6 +129,10 @@ module Quant
147
129
 
148
130
  body_ratio < 0.025 && head_ratio > 1.0 && tail_ratio > 1.0
149
131
  end
132
+
133
+ 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}>"
135
+ end
150
136
  end
151
137
  end
152
138
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Ticks
5
+ module Serializers
6
+ module OHLC
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 key properties for the tick.
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,79 @@
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
+ # Two ticks are equal if they have the same close price and close timestamp.
64
+ def ==(other)
65
+ [close_price, close_timestamp] == [other.close_price, other.close_timestamp]
17
66
  end
18
67
 
68
+ # The corresponding? method helps determine that the other tick's timestamp is the same as this tick's timestamp,
69
+ # which is useful when aligning ticks between two separate series where one starts or ends at a different time,
70
+ # or when there may be gaps in the data between the two series.
19
71
  def corresponding?(other)
20
72
  close_timestamp == other.close_timestamp
21
73
  end
22
74
 
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 }
75
+ def inspect
76
+ "#<#{self.class.name} #{interval} ct=#{close_timestamp} c=#{close_price.to_f} v=#{volume}>"
30
77
  end
31
78
  end
32
79
  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.1"
4
+ VERSION = "0.1.3"
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")
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.1
4
+ version: 0.1.3
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-12 00:00:00.000000000 Z
11
+ date: 2024-02-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -56,12 +56,15 @@ files:
56
56
  - lib/quant/mixins/trig.rb
57
57
  - lib/quant/mixins/weighted_average.rb
58
58
  - lib/quant/refinements/array.rb
59
- - lib/quant/ticks/core.rb
60
- - lib/quant/ticks/indicator_points.rb
59
+ - lib/quant/security.rb
60
+ - lib/quant/security_class.rb
61
+ - lib/quant/series.rb
61
62
  - lib/quant/ticks/ohlc.rb
62
- - lib/quant/ticks/serializers/value.rb
63
+ - lib/quant/ticks/serializers/ohlc.rb
64
+ - lib/quant/ticks/serializers/spot.rb
65
+ - lib/quant/ticks/serializers/tick.rb
63
66
  - lib/quant/ticks/spot.rb
64
- - lib/quant/ticks/value.rb
67
+ - lib/quant/ticks/tick.rb
65
68
  - lib/quant/time_methods.rb
66
69
  - lib/quant/time_period.rb
67
70
  - lib/quant/version.rb
@@ -89,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
89
92
  - !ruby/object:Gem::Version
90
93
  version: '0'
91
94
  requirements: []
92
- rubygems_version: 3.5.5
95
+ rubygems_version: 3.5.6
93
96
  signing_key:
94
97
  specification_version: 4
95
98
  summary: Quantitative and statistical tools written for Ruby 3.x for trading and finance.
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Quant
4
- module Ticks
5
- class Core
6
- end
7
- end
8
- end
@@ -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