quantitative 0.1.1 → 0.1.3

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: 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