mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-05 13:35:21 +02:00
Data provider simplification, tests, and documentation (#1997)
* Ignore env.test from source control * Simplification of providers interface * Synth tests * Update money to use new find rates method * Remove unused issues code * Additional issue feature removals * Update price data fetching and tests * Update documentation for providers * Security test fixes * Fix self host test * Update synth usage data access * Remove AI pr schema changes
This commit is contained in:
parent
dd75cadebc
commit
f65b93a352
95 changed files with 2014 additions and 1638 deletions
|
@ -1,33 +1,6 @@
|
|||
class Security::Price < ApplicationRecord
|
||||
include Provided
|
||||
|
||||
belongs_to :security
|
||||
|
||||
validates :price, :currency, presence: true
|
||||
|
||||
class << self
|
||||
def find_price(security:, date:, cache: true)
|
||||
result = find_by(security:, date:)
|
||||
|
||||
result || fetch_price_from_provider(security:, date:, cache:)
|
||||
end
|
||||
|
||||
def find_prices(security:, start_date:, end_date: Date.current, cache: true)
|
||||
prices = where(security_id: security.id, date: start_date..end_date).to_a
|
||||
all_dates = (start_date..end_date).to_a.to_set
|
||||
existing_dates = prices.map(&:date).to_set
|
||||
missing_dates = (all_dates - existing_dates).sort
|
||||
|
||||
if missing_dates.any?
|
||||
prices += fetch_prices_from_provider(
|
||||
security: security,
|
||||
start_date: missing_dates.first,
|
||||
end_date: missing_dates.last,
|
||||
cache: cache
|
||||
)
|
||||
end
|
||||
|
||||
prices
|
||||
end
|
||||
end
|
||||
validates :date, :price, :currency, presence: true
|
||||
validates :date, uniqueness: { scope: %i[security_id currency] }
|
||||
end
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
module Security::Price::Provided
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
include Synthable
|
||||
|
||||
class_methods do
|
||||
def provider
|
||||
synth_client
|
||||
end
|
||||
|
||||
private
|
||||
def fetch_price_from_provider(security:, date:, cache: false)
|
||||
return nil unless provider.present?
|
||||
return nil unless security.has_prices?
|
||||
|
||||
response = provider.fetch_security_prices \
|
||||
ticker: security.ticker,
|
||||
mic_code: security.exchange_operating_mic,
|
||||
start_date: date,
|
||||
end_date: date
|
||||
|
||||
if response.success? && response.prices.size > 0
|
||||
price = Security::Price.new \
|
||||
security: security,
|
||||
date: response.prices.first[:date],
|
||||
price: response.prices.first[:price],
|
||||
currency: response.prices.first[:currency]
|
||||
|
||||
price.save! if cache
|
||||
price
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_prices_from_provider(security:, start_date:, end_date:, cache: false)
|
||||
return [] unless provider.present?
|
||||
return [] unless security
|
||||
return [] unless security.has_prices?
|
||||
|
||||
response = provider.fetch_security_prices \
|
||||
ticker: security.ticker,
|
||||
mic_code: security.exchange_operating_mic,
|
||||
start_date: start_date,
|
||||
end_date: end_date
|
||||
|
||||
if response.success?
|
||||
response.prices.map do |price|
|
||||
new_price = Security::Price.find_or_initialize_by(
|
||||
security: security,
|
||||
date: price[:date]
|
||||
) do |p|
|
||||
p.price = price[:price]
|
||||
p.currency = price[:currency]
|
||||
end
|
||||
|
||||
new_price.save! if cache && new_price.new_record?
|
||||
new_price
|
||||
end
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
31
app/models/security/provideable.rb
Normal file
31
app/models/security/provideable.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
module Security::Provideable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
Search = Data.define(:securities)
|
||||
PriceData = Data.define(:price)
|
||||
PricesData = Data.define(:prices)
|
||||
SecurityInfo = Data.define(
|
||||
:ticker,
|
||||
:name,
|
||||
:links,
|
||||
:logo_url,
|
||||
:description,
|
||||
:kind,
|
||||
)
|
||||
|
||||
def search_securities(symbol, country_code: nil, exchange_operating_mic: nil)
|
||||
raise NotImplementedError, "Subclasses must implement #search_securities"
|
||||
end
|
||||
|
||||
def fetch_security_info(security)
|
||||
raise NotImplementedError, "Subclasses must implement #fetch_security_info"
|
||||
end
|
||||
|
||||
def fetch_security_price(security, date:)
|
||||
raise NotImplementedError, "Subclasses must implement #fetch_security_price"
|
||||
end
|
||||
|
||||
def fetch_security_prices(security, start_date:, end_date:)
|
||||
raise NotImplementedError, "Subclasses must implement #fetch_security_prices"
|
||||
end
|
||||
end
|
|
@ -1,28 +1,65 @@
|
|||
module Security::Provided
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
include Synthable
|
||||
|
||||
class_methods do
|
||||
def provider
|
||||
synth_client
|
||||
Providers.synth
|
||||
end
|
||||
|
||||
def search_provider(query)
|
||||
return [] if query[:search].blank? || query[:search].length < 2
|
||||
def search_provider(symbol, country_code: nil, exchange_operating_mic: nil)
|
||||
return [] if symbol.blank? || symbol.length < 2
|
||||
|
||||
response = provider.search_securities(
|
||||
query: query[:search],
|
||||
dataset: "limited",
|
||||
country_code: query[:country],
|
||||
exchange_operating_mic: query[:exchange_operating_mic]
|
||||
)
|
||||
response = provider.search_securities(symbol, country_code: country_code, exchange_operating_mic: exchange_operating_mic)
|
||||
|
||||
if response.success?
|
||||
response.securities.map { |attrs| new(**attrs) }
|
||||
response.data.securities
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def sync_provider_prices(start_date:, end_date: Date.current)
|
||||
unless has_prices?
|
||||
Rails.logger.warn("Security id=#{id} ticker=#{ticker} is not known by provider, skipping price sync")
|
||||
return 0
|
||||
end
|
||||
|
||||
unless provider.present?
|
||||
Rails.logger.warn("No security provider configured, cannot sync prices for id=#{id} ticker=#{ticker}")
|
||||
return 0
|
||||
end
|
||||
|
||||
response = provider.fetch_security_prices(self, start_date: start_date, end_date: end_date)
|
||||
|
||||
unless response.success?
|
||||
Rails.logger.error("Provider error for sync_provider_prices with id=#{id} ticker=#{ticker}: #{response.error}")
|
||||
return 0
|
||||
end
|
||||
|
||||
fetched_prices = response.data.prices.map do |price|
|
||||
price.attributes.slice("security_id", "date", "price", "currency")
|
||||
end
|
||||
|
||||
Security::Price.upsert_all(fetched_prices, unique_by: %i[security_id date currency])
|
||||
end
|
||||
|
||||
def find_or_fetch_price(date: Date.current, cache: true)
|
||||
price = prices.find_by(date: date)
|
||||
|
||||
return price if price.present?
|
||||
|
||||
response = provider.fetch_security_price(self, date: date)
|
||||
|
||||
return nil unless response.success? # Provider error
|
||||
|
||||
price = response.data.price
|
||||
price.save! if cache
|
||||
price
|
||||
end
|
||||
|
||||
private
|
||||
def provider
|
||||
self.class.provider
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue