market_data 0.2.1 → 0.3.0

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: b63212c9e8ada21f198021bc3537392d9a6a34933682ebe789923883c2f38131
4
- data.tar.gz: 9afa24ef746a81f115628f65aff4bccfdbfbf3086887444d62409545be76b7fb
3
+ metadata.gz: c83d3644178f120e069d283ebfc132d543ac8dbe2aca9734388d2cd56274bf7c
4
+ data.tar.gz: eeef52d93f45e64b4869749bd787b0af576924a0b617e8897c4eada6dcf813b2
5
5
  SHA512:
6
- metadata.gz: e21e172593fcee9340de7ca5ac37373db2d07479e2c912c01d7bf05399e012edcf405caf12c2cbf3ad0545b4ae8a166d6c98f40f67367e60040f818356866666
7
- data.tar.gz: a3fcd6d6368c073621e9f400be5412b82e88ada5cce2b00a26b4e6697b97b6beb568564f64032c6c8eb777db93fe7afa3f324c616465e2913b838fd02ffa3eed
6
+ metadata.gz: b01a71a756244d6461e19cad34abb3b25df2705b43e03e47a936a66a471879f8ae0038e49ab01f062ec0db4c046e40e2d80d3162a5c065f535ffa7877efed548
7
+ data.tar.gz: fe1a289aa65eb0e6a0682b9e263a232f0eb2b913a808b9c6ac5271e46b52123a4c805a586bc5b2a1b9aeda92c17fb640815e6230907a9dcf6a0c5379bcd34b59
data/CHANGELOG.md CHANGED
@@ -24,4 +24,9 @@
24
24
  ## [0.2.1] - 2024-10-09
25
25
 
26
26
  - Fix broken tests
27
- - Add support for new optional parameters for `quotes` and `bulk_quotes` endpoint
27
+ - Add support for new optional parameters for `quotes` and `bulk_quotes` endpoint
28
+
29
+ ## [0.3.0] - 2024-10-09
30
+
31
+ - Add support for Earnings endpoint under the `earnings` method
32
+ - Introduced `Validations` module for parameter validation logic
data/README.md CHANGED
@@ -2,12 +2,11 @@
2
2
 
3
3
  A Ruby wrapper for the [MarketData API](https://www.marketdata.app/docs/api).
4
4
 
5
- ![coverage](https://img.shields.io/badge/coverage%3A-84.71%25-yellow.svg)
5
+ ![coverage](https://img.shields.io/badge/coverage%3A-87.77%25-yellow.svg)
6
6
 
7
7
  ## Installation
8
8
 
9
9
  $ gem install market_data
10
- $ bundle install
11
10
 
12
11
  ## Usage
13
12
 
@@ -93,7 +92,7 @@ For the `bulk_candles` method you pass a array of ticker symbols. Resolution is
93
92
 
94
93
  It returns a hashmap with the ticker symbol as a key.
95
94
 
96
- $ candles = cl.bulk_candles(["AAPL", "AMD", "NOTAQUOTE"])
95
+ $ candles = client.bulk_candles(["AAPL", "AMD", "NOTAQUOTE"])
97
96
 
98
97
  $ candles["AMD"]
99
98
  $ => #<struct MarketData::Models::Candle symbol="AMD", open=174.05, high=174.05, low=169.55, close=171.02, volume=33391035, time=1728446400>
@@ -103,6 +102,30 @@ It returns a hashmap with the ticker symbol as a key.
103
102
 
104
103
  If a quote is not found, the hashmap will return a nil value for that ticker's key.
105
104
 
105
+ ### Earnings
106
+
107
+ See the API [docs](https://www.marketdata.app/docs/api/stocks/earnings) for parameter specification.
108
+
109
+ $ client.earnings("AAPL", from: (Time.now - MarketData::Constants::YEAR).iso8601, to: Time.now.iso8601, countback: nil, report: nil, date: nil)
110
+ $ => [#<struct MarketData::Models::Earning
111
+ symbol="AAPL",
112
+ fiscal_year=2023,
113
+ fiscal_quarter=4,
114
+ date=1696046400,
115
+ report_date=1698897600,
116
+ report_time="after close",
117
+ currency="USD",
118
+ reported_eps=1.46,
119
+ estimated_eps=1.39,
120
+ surprise_eps=0.07,
121
+ surprise_eps_pct=0.0504,
122
+ updated=1728273600>,
123
+ #<struct MarketData::Models::Earning
124
+ symbol="AAPL",
125
+ fiscal_year=2024,
126
+ fiscal_quarter=1,
127
+ ...
128
+
106
129
  ## ROADMAP
107
130
 
108
131
  The following is an ordered list of next expected developments, based on the endpoints present in the [docs](https://www.marketdata.app/docs/api)
@@ -113,7 +136,7 @@ From Stocks endpoints:
113
136
  - [X] Candles
114
137
  - [X] Bulk Candles
115
138
  - [X] Support for new optional parameters for Quotes and Bulk Quotes
116
- - [ ] Earnings
139
+ - [X] Earnings
117
140
 
118
141
  From Markets endpoints:
119
142
  - [ ] Status
@@ -5,9 +5,47 @@ module MarketData
5
5
  HOUR = MINUTE * 60
6
6
  DAY = HOUR * 24
7
7
  WEEK = DAY * 7
8
+ MONTH_30 = DAY * 30
9
+ MONTH_31 = DAY * 31
8
10
  YEAR = DAY * 365
9
11
 
10
- QUOTE_FIELDS = [:symbol, :ask, :askSize, :bid, :bidSize, :mid, :last, :change, :changepct, :volume, :updated, :high52, :low52]
11
- CANDLE_FIELDS = [:symbol, :open, :high, :low, :close, :volume, :time]
12
+ EARNING_FIELD_MAPPING = {
13
+ symbol: "symbol",
14
+ fiscal_year: "fiscalYear",
15
+ fiscal_quarter: "fiscalQuarter",
16
+ date: "date",
17
+ report_date: "reportDate",
18
+ report_time: "reportTime",
19
+ currency: "currency",
20
+ reported_eps: "reportedEPS",
21
+ estimated_eps: "estimatedEPS",
22
+ surprise_eps: "surpriseEPS",
23
+ surprise_eps_pct: "surpriseEPSpct",
24
+ updated: "updated"
25
+ }
26
+ CANDLE_FIELD_MAPPING = {
27
+ symbol: "symbol",
28
+ open: "o",
29
+ close: "c",
30
+ low: "l",
31
+ high: "h",
32
+ volume: "v",
33
+ time: "t",
34
+ }
35
+ QUOTE_FIELD_MAPPING = {
36
+ symbol: "symbol",
37
+ ask: "ask",
38
+ ask_size: "askSize",
39
+ bid: "bid",
40
+ bid_size: "bidSize",
41
+ mid: "mid",
42
+ last: "last",
43
+ change: "change",
44
+ change_pct: "changepct",
45
+ volume: "volume",
46
+ updated: "updated",
47
+ high52: "high52",
48
+ low52: "low52",
49
+ }
12
50
  end
13
51
  end
@@ -3,32 +3,16 @@ require 'market_data/models'
3
3
  module MarketData
4
4
  module Mappers
5
5
  include MarketData::Models
6
-
7
6
  SYMBOL_RESPONSE_KEY = "symbol"
8
7
  STATUS_RESPONSE_KEY = "s"
9
8
 
10
9
  def map_quote response, i=0
11
- Quote.new(
12
- symbol: response["symbol"][i],
13
- ask: response["ask"][i],
14
- askSize: response["askSize"][i],
15
- bid: response["bid"][i],
16
- bidSize: response["bidSize"][i],
17
- mid: response["mid"][i],
18
- last: response["last"][i],
19
- change: response["change"][i],
20
- changepct: response["changepct"][i],
21
- volume: response["volume"][i],
22
- updated: response["updated"][i],
23
- high52: response.fetch("high52", nil),
24
- low52: response.fetch("low52", nil),
25
- )
10
+ Quote.new(**map_fields_for(response, :quote, i))
26
11
  end
27
12
 
28
13
  def map_bulk_quotes response
29
14
  h = Hash.new
30
- size = response[SYMBOL_RESPONSE_KEY].size
31
- for i in 0..(size - 1) do
15
+ (0..(response[SYMBOL_RESPONSE_KEY].size - 1)).each do |i|
32
16
  qquote = map_quote(response, i)
33
17
  h[response[SYMBOL_RESPONSE_KEY][i]] = !qquote.blank? ? qquote : nil
34
18
  end
@@ -37,31 +21,50 @@ module MarketData
37
21
 
38
22
  def map_candles response, symbol
39
23
  ar = []
40
- size = response["o"].size
41
-
42
- for i in 0..(size - 1) do
43
- ar << Candle.new(
44
- open: response["o"][i],
45
- high: response["h"][i],
46
- low: response["l"][i],
47
- close: response["c"][i],
48
- volume: response["v"][i],
49
- time: response["t"][i],
50
- symbol: symbol
51
- )
24
+ (0..(response["o"].size - 1)).each do |i|
25
+ args = map_fields_for(response, :candle, i)
26
+ args[:symbol] = symbol
27
+ ar << Candle.new(**args)
52
28
  end
29
+
53
30
  ar
54
31
  end
55
32
 
56
33
  def map_bulk_candles response
57
34
  h = Hash.new
58
- size = response[SYMBOL_RESPONSE_KEY].size
59
-
60
- for i in 0..(size - 1) do
61
- candle = Candle.new(symbol: response["symbol"][i], open: response["o"][i], high: response["h"][i], low: response["l"][i], close: response["c"][i], volume: response["v"][i], time: response["t"][i])
35
+ (0..(response[SYMBOL_RESPONSE_KEY].size - 1)).each do |i|
36
+ candle = Candle.new(**map_fields_for(response, :candle, i))
62
37
  h[response[SYMBOL_RESPONSE_KEY][i]] = !candle.blank? ? candle : nil
63
38
  end
64
39
  h
65
40
  end
41
+
42
+ def map_earning response
43
+ ar = []
44
+ (0..(response[SYMBOL_RESPONSE_KEY].size - 1)).each do |i|
45
+ ar << Earning.new(**map_fields_for(response, :earning, i))
46
+ end
47
+ ar
48
+ end
49
+
50
+ def map_fields_for(response, kind, i=0)
51
+ mapping = {}
52
+ case kind
53
+ when :candle
54
+ mapping = Constants::CANDLE_FIELD_MAPPING
55
+ when :earning
56
+ mapping = Constants::EARNING_FIELD_MAPPING
57
+ when :quote
58
+ mapping = Constants::QUOTE_FIELD_MAPPING
59
+ else
60
+ raise BadParameterError.new("unrecognized model for mapping: #{kind}")
61
+ end
62
+
63
+ r = {}
64
+ mapping.each do |field, mapped|
65
+ r.store(field, response.fetch(mapped, nil).nil? ? nil : response.fetch(mapped)[i])
66
+ end
67
+ r
68
+ end
66
69
  end
67
70
  end
@@ -2,18 +2,18 @@ require 'market_data/constants'
2
2
 
3
3
  module MarketData
4
4
  module Models
5
- include MarketData::Constants
6
-
7
- Quote = Struct.new(*Constants::QUOTE_FIELDS) do
5
+ Quote = Struct.new(*Constants::QUOTE_FIELD_MAPPING.keys) do
8
6
  def blank?
9
- (QUOTE_FIELDS - [:symbol]).all? { |mmethod| self[mmethod].nil?}
7
+ (Constants::QUOTE_FIELD_MAPPING.keys - [:symbol]).all? { |mmethod| self[mmethod].nil?}
10
8
  end
11
9
  end
12
10
 
13
- Candle = Struct.new(*Constants::CANDLE_FIELDS) do
11
+ Candle = Struct.new(*Constants::CANDLE_FIELD_MAPPING.keys) do
14
12
  def blank?
15
- (CANDLE_FIELDS - [:symbol]).all? { |mmethod| self[mmethod].nil?}
13
+ (Constants::CANDLE_FIELD_MAPPING.keys - [:symbol]).all? { |mmethod| self[mmethod].nil?}
16
14
  end
17
15
  end
16
+
17
+ Earning = Struct.new(*Constants::EARNING_FIELD_MAPPING.keys)
18
18
  end
19
19
  end
@@ -1,53 +1,38 @@
1
1
  require 'market_data/conn'
2
2
  require 'market_data/errors'
3
3
  require 'market_data/mappers'
4
+ require 'market_data/validations'
4
5
 
5
6
  module MarketData
6
7
  module Quotes
7
8
  include MarketData::Mappers
8
9
  include MarketData::Errors
9
10
  include MarketData::Conn
11
+ include MarketData::Validations
10
12
 
11
13
  @@single = "/v1/stocks/quotes/"
12
14
  @@bulk = "/v1/stocks/bulkquotes/"
13
15
  @@candles = "/v1/stocks/candles/"
14
16
  @@bulk_candles = "/v1/stocks/bulkcandles/"
17
+ @@earnings = "/v1/stocks/earnings/"
15
18
 
16
19
  def quote(symbol, w52 = false, extended = false)
20
+ query = validate_quotes_input!(symbol: symbol, w52: w52, extended: extended)
21
+
17
22
  path_hash = { host: MarketData.base_host, path: @@single + symbol }
18
- query_hash = {}
19
- if w52
20
- query_hash["52week"] = true
21
- end
22
- # MarketData API considers extended as true by default
23
- if !extended
24
- query_hash[:extended] = false
25
- end
26
- if !query_hash.empty?
27
- path_hash[:query] = URI.encode_www_form(query_hash)
23
+ if !query.empty?
24
+ path_hash[:query] = URI.encode_www_form(query)
28
25
  end
26
+
29
27
  res = do_connect(get_uri path_hash)
30
28
  map_quote(res)
31
29
  end
32
30
 
33
31
  def bulk_quotes(symbols, snapshot = false, extended = false)
34
- path_hash = { host: MarketData.base_host, path: @@bulk }
35
- query_hash = {}
32
+ query = validate_bulk_quotes_input!(symbols: symbols, snapshot: snapshot, extended: extended)
36
33
 
37
- # MarketData API considers extended as true by default
38
- if !extended
39
- query_hash[:extended] = false
40
- end
41
-
42
- if snapshot
43
- query_hash[:snapshot] = true
44
- else
45
- if !symbols.is_a?(Array) || symbols.size < 1
46
- raise BadParameterError.new("symbols must be a non-empty list")
47
- end
48
- query_hash[:symbols] = symbols.join(",")
49
- end
50
- path_hash[:query] = URI.encode_www_form(query_hash)
34
+ path_hash = { host: MarketData.base_host, path: @@bulk }
35
+ path_hash[:query] = URI.encode_www_form(query)
51
36
 
52
37
  res = do_connect(get_uri path_hash)
53
38
  map_bulk_quotes res
@@ -56,43 +41,33 @@ module MarketData
56
41
  def candles(symbol, opts = {})
57
42
  defaults = {resolution: "D", from: nil, to: Time.now.utc.to_i, countback: nil}
58
43
  opts = defaults.merge(opts)
59
-
60
- query_hash = {to: opts[:to]}
61
-
62
- # TODO Move method validations into own class
63
- # TODO check to is either iso8601 or unix
64
- if opts[:from].nil? && opts[:countback].nil?
65
- raise BadParameterError.new("either :from or :countback must be supplied")
66
- end
67
-
68
- if opts[:from].nil?
69
- query_hash[:countback] = opts[:countback]
70
- else
71
- query_hash[:from] = opts[:from]
72
- end
44
+ query = validate_candles_input!(**opts)
73
45
 
74
46
  path_hash = { host: MarketData.base_host, path: @@candles + opts[:resolution] + "/" + symbol }
75
- path_hash[:query] = URI.encode_www_form(query_hash)
47
+ path_hash[:query] = URI.encode_www_form(query)
76
48
 
77
49
  res = do_connect(get_uri path_hash)
78
50
  map_candles res, symbol
79
51
  end
80
52
 
81
53
  def bulk_candles(symbols, resolution = "D")
82
- unless resolution == "daily" || resolution == "1D" || resolution == "D"
83
- raise BadParameterError.new("only daily resolution is allowed for this endpoint")
84
- end
54
+ query = validate_bulk_candles_input!(symbols: symbols, resolution: resolution)
55
+ query = query.except(:resolution)
56
+
85
57
  path_hash = { host: MarketData.base_host, path: @@bulk_candles + resolution + "/" }
58
+ path_hash[:query] = URI.encode_www_form(query)
86
59
 
87
- if !symbols.is_a?(Array) || symbols.size < 1
88
- raise BadParameterError.new("symbols must be a non-empty list")
89
- end
90
- query_hash = { symbols: symbols.join(",") }
60
+ res = do_connect(get_uri path_hash)
61
+ map_bulk_candles res
62
+ end
91
63
 
92
- path_hash[:query] = URI.encode_www_form(query_hash)
64
+ def earnings(symbol, opts = {from: nil, to: nil, countback: nil, date: nil, report: nil})
65
+ path_hash = { host: MarketData.base_host(), path: @@earnings + symbol}
66
+ query = validate_earnings_input!(**opts)
67
+ path_hash[:query] = URI.encode_www_form(query)
93
68
 
94
69
  res = do_connect(get_uri path_hash)
95
- map_bulk_candles res
70
+ map_earning res
96
71
  end
97
72
  end
98
73
  end
@@ -0,0 +1,128 @@
1
+ require 'date'
2
+
3
+ module MarketData
4
+ module Validations
5
+ include MarketData::Errors
6
+
7
+ VALID_DAILY_RESOLUTION = ['daily', 'D', '1D', '2D', '3D', '4D', '5D']
8
+ VALID_RESOLUTIONS = [
9
+ *VALID_DAILY_RESOLUTION,
10
+ 'weekly', 'W', '1W', '2W', '3W', '4W', '5W',
11
+ 'monthly', 'M', '1M', '2M', '3M', '4M', '5M',
12
+ 'yearly', 'Y', '1Y', '2Y', '3Y', '4Y', '5Y',
13
+ ]
14
+
15
+ def validate_quotes_input!(symbol: nil, w52: nil, extended: nil)
16
+ result = {}
17
+
18
+ if w52
19
+ result.merge!({"52week" => true})
20
+ end
21
+ # MarketData API considers extended as true by default. Should be included
22
+ # in the query when false
23
+ if !extended
24
+ result.merge!({extended: false})
25
+ end
26
+
27
+ result
28
+ end
29
+
30
+ def validate_bulk_quotes_input!(symbols: nil, snapshot: nil, extended: nil)
31
+ result = {extended: false}
32
+ if snapshot
33
+ result.merge!({snapshot: true})
34
+ else
35
+ if !symbols.kind_of?(Array) || symbols.size < 2
36
+ raise BadParameterError.new("symbols must be list with at least 2 symbols")
37
+ end
38
+ result.merge!({symbols: symbols.join(",")})
39
+ end
40
+ if extended
41
+ result.merge!({extended: true})
42
+ end
43
+
44
+ result
45
+ end
46
+
47
+ def validate_bulk_candles_input!(resolution: nil, symbols: nil)
48
+ s, r = validate_resolution(resolution, VALID_DAILY_RESOLUTION)
49
+ if s == :invalid
50
+ raise BadParameterError.new(r)
51
+ end
52
+ if !symbols.kind_of?(Array) || symbols.size < 2
53
+ raise BadParameterError.new("symbols must be list with at least 2 symbols")
54
+ end
55
+
56
+ r.merge({symbols: symbols.join(",")})
57
+ end
58
+
59
+ def validate_candles_input!(
60
+ resolution: nil, from: nil, to: nil, countback: nil
61
+ )
62
+
63
+ state, response = validate_from_to_countback_strategy(from: from, to: to, countback: countback)
64
+ if state == :invalid
65
+ raise BadParameterError.new(response)
66
+ end
67
+
68
+ state, res = validate_resolution(resolution)
69
+ if state == :invalid
70
+ raise BadParameterError.new(res)
71
+ end
72
+
73
+ response.merge(res)
74
+ end
75
+
76
+ def validate_earnings_input!(
77
+ from: nil, to: nil, countback: nil, date: nil, report: nil
78
+ )
79
+ if !date.nil?
80
+ return {date: date}
81
+ end
82
+ if !report.nil?
83
+ return {report: report}
84
+ end
85
+
86
+ state, response = validate_from_to_countback_strategy(from: from, to: to, countback: countback)
87
+ if state == :invalid
88
+ raise BadParameterError.new(response)
89
+ end
90
+
91
+ return response
92
+ end
93
+
94
+ def validate_from_to_countback_strategy(
95
+ from: nil, to: nil, countback: nil
96
+ )
97
+ if !from.nil? && !to.nil?
98
+ return :valid, {from: from, to: to}
99
+ end
100
+ if !to.nil? && !countback.nil? && from.nil?
101
+ return :valid, {to: to, countback: countback}
102
+ end
103
+
104
+ return :invalid, "supply either :from and :to, or :to and :countback"
105
+ end
106
+
107
+ def validate_resolution resolution, allowed_values = VALID_RESOLUTIONS
108
+ if VALID_RESOLUTIONS.include? resolution
109
+ return :valid, {resolution: resolution}
110
+ end
111
+ return :invalid, "invalid resolution: #{resolution}"
112
+ end
113
+
114
+ def time_valid?(t)
115
+ if t.kind_of?(String)
116
+ begin
117
+ DateTime.iso8601(t)
118
+ return true
119
+ rescue
120
+ return false
121
+ end
122
+ end
123
+ if t.kind_of?(Integer)
124
+ return t > 0
125
+ end
126
+ end
127
+ end
128
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MarketData
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: market_data
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastián González
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-09 00:00:00.000000000 Z
11
+ date: 2024-10-11 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A Ruby client for the MarketData API.
14
14
  email:
@@ -31,6 +31,7 @@ files:
31
31
  - lib/market_data/mappers.rb
32
32
  - lib/market_data/models.rb
33
33
  - lib/market_data/quotes.rb
34
+ - lib/market_data/validations.rb
34
35
  - lib/market_data/version.rb
35
36
  - market_data.gemspec
36
37
  - sig/market_data.rbs