1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-02 20:15:22 +02:00

Market data sync refinements (#2252)

* Exchange rate syncer implementation

* Security price syncer

* Fix issues with provider API

* Add back prod schedule

* Add back price and exchange rate syncs to account syncs

* Remove unused stock_exchanges table
This commit is contained in:
Zach Gollwitzer 2025-05-16 14:17:56 -04:00 committed by GitHub
parent 6917cecf33
commit 6dc1d22672
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1206 additions and 1615 deletions

View file

@ -2,22 +2,22 @@ module Provider::SecurityConcept
extend ActiveSupport::Concern
Security = Data.define(:symbol, :name, :logo_url, :exchange_operating_mic)
SecurityInfo = Data.define(:symbol, :name, :links, :logo_url, :description, :kind)
Price = Data.define(:security, :date, :price, :currency)
SecurityInfo = Data.define(:symbol, :name, :links, :logo_url, :description, :kind, :exchange_operating_mic)
Price = Data.define(:symbol, :date, :price, :currency, :exchange_operating_mic)
def search_securities(symbol, country_code: nil, exchange_operating_mic: nil)
raise NotImplementedError, "Subclasses must implement #search_securities"
end
def fetch_security_info(security)
def fetch_security_info(symbol:, exchange_operating_mic:)
raise NotImplementedError, "Subclasses must implement #fetch_security_info"
end
def fetch_security_price(security, date:)
def fetch_security_price(symbol:, exchange_operating_mic:, date:)
raise NotImplementedError, "Subclasses must implement #fetch_security_price"
end
def fetch_security_prices(security, start_date:, end_date:)
def fetch_security_prices(symbol:, exchange_operating_mic:, start_date:, end_date:)
raise NotImplementedError, "Subclasses must implement #fetch_security_prices"
end
end

View file

@ -3,6 +3,8 @@ class Provider::Synth < Provider
# Subclass so errors caught in this provider are raised as Provider::Synth::Error
Error = Class.new(Provider::Error)
InvalidExchangeRateError = Class.new(Error)
InvalidSecurityPriceError = Class.new(Error)
def initialize(api_key)
@api_key = api_key
@ -48,7 +50,7 @@ class Provider::Synth < Provider
rates = JSON.parse(response.body).dig("data", "rates")
Rate.new(date:, from:, to:, rate: rates.dig(to))
Rate.new(date: date.to_date, from:, to:, rate: rates.dig(to))
end
end
@ -65,8 +67,18 @@ class Provider::Synth < Provider
end
data.paginated.map do |rate|
Rate.new(date: rate.dig("date"), from:, to:, rate: rate.dig("rates", to))
end
date = rate.dig("date")
rate = rate.dig("rates", to)
if date.nil? || rate.nil?
message = "#{self.class.name} returned invalid rate data for pair from: #{from} to: #{to} on: #{date}. Rate data: #{rate.inspect}"
Rails.logger.warn(message)
Sentry.capture_exception(InvalidExchangeRateError.new(message), level: :warning)
next
end
Rate.new(date: date.to_date, from:, to:, rate:)
end.compact
end
end
@ -97,65 +109,73 @@ class Provider::Synth < Provider
end
end
def fetch_security_info(security)
def fetch_security_info(symbol:, exchange_operating_mic:)
with_provider_response do
response = client.get("#{base_url}/tickers/#{security.ticker}") do |req|
req.params["mic_code"] = security.exchange_mic if security.exchange_mic.present?
req.params["operating_mic"] = security.exchange_operating_mic if security.exchange_operating_mic.present?
response = client.get("#{base_url}/tickers/#{symbol}") do |req|
req.params["operating_mic"] = exchange_operating_mic
end
data = JSON.parse(response.body).dig("data")
SecurityInfo.new(
symbol: data.dig("ticker"),
symbol: symbol,
name: data.dig("name"),
links: data.dig("links"),
logo_url: data.dig("logo_url"),
description: data.dig("description"),
kind: data.dig("kind")
kind: data.dig("kind"),
exchange_operating_mic: exchange_operating_mic
)
end
end
def fetch_security_price(security, date:)
def fetch_security_price(symbol:, exchange_operating_mic:, date:)
with_provider_response do
historical_data = fetch_security_prices(security, start_date: date, end_date: date)
historical_data = fetch_security_prices(symbol:, exchange_operating_mic:, start_date: date, end_date: date)
raise ProviderError, "No prices found for security #{security.ticker} on date #{date}" if historical_data.data.empty?
raise ProviderError, "No prices found for security #{symbol} on date #{date}" if historical_data.data.empty?
historical_data.data.first
end
end
def fetch_security_prices(security, start_date:, end_date:)
def fetch_security_prices(symbol:, exchange_operating_mic:, start_date:, end_date:)
with_provider_response do
params = {
start_date: start_date,
end_date: end_date
end_date: end_date,
operating_mic_code: exchange_operating_mic
}
params[:operating_mic_code] = security.exchange_operating_mic if security.exchange_operating_mic.present?
data = paginate(
"#{base_url}/tickers/#{security.ticker}/open-close",
"#{base_url}/tickers/#{symbol}/open-close",
params
) do |body|
body.dig("prices")
end
currency = data.first_page.dig("currency")
country_code = data.first_page.dig("exchange", "country_code")
exchange_mic = data.first_page.dig("exchange", "mic_code")
exchange_operating_mic = data.first_page.dig("exchange", "operating_mic_code")
data.paginated.map do |price|
date = price.dig("date")
price = price.dig("close") || price.dig("open")
if date.nil? || price.nil?
message = "#{self.class.name} returned invalid price data for security #{symbol} on: #{date}. Price data: #{price.inspect}"
Rails.logger.warn(message)
Sentry.capture_exception(InvalidSecurityPriceError.new(message), level: :warning)
next
end
Price.new(
security: security,
date: price.dig("date"),
price: price.dig("close") || price.dig("open"),
currency: currency
symbol: symbol,
date: date.to_date,
price: price,
currency: currency,
exchange_operating_mic: exchange_operating_mic
)
end
end.compact
end
end