mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-05 05:25:24 +02:00
First pass at security price reference (#1388)
* First pass at security price reference
* Data cleanup
* Synth security fetching does better with a mic_code
* Update test suite
😭
* Update schema.rb
* Update generator.rb
This commit is contained in:
parent
bf695972e4
commit
490f44589e
16 changed files with 155 additions and 88 deletions
|
@ -38,23 +38,31 @@ class Account::Holding::Syncer
|
|||
|
||||
def security_prices
|
||||
@security_prices ||= begin
|
||||
prices = {}
|
||||
ticker_start_dates = {}
|
||||
prices = {}
|
||||
ticker_securities = {}
|
||||
|
||||
sync_entries.each do |entry|
|
||||
unless ticker_start_dates[entry.account_trade.security.ticker]
|
||||
ticker_start_dates[entry.account_trade.security.ticker] = entry.date
|
||||
end
|
||||
end
|
||||
sync_entries.each do |entry|
|
||||
security = entry.account_trade.security
|
||||
unless ticker_securities[security.ticker]
|
||||
ticker_securities[security.ticker] = {
|
||||
security: security,
|
||||
start_date: entry.date
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
ticker_start_dates.each do |ticker, date|
|
||||
fetched_prices = Security::Price.find_prices(ticker: ticker, start_date: date, end_date: Date.current)
|
||||
gapfilled_prices = Gapfiller.new(fetched_prices, start_date: date, end_date: Date.current, cache: false).run
|
||||
prices[ticker] = gapfilled_prices
|
||||
end
|
||||
ticker_securities.each do |ticker, data|
|
||||
fetched_prices = Security::Price.find_prices(
|
||||
security: data[:security],
|
||||
start_date: data[:start_date],
|
||||
end_date: Date.current
|
||||
)
|
||||
gapfilled_prices = Gapfiller.new(fetched_prices, start_date: data[:start_date], end_date: Date.current, cache: false).run
|
||||
prices[ticker] = gapfilled_prices
|
||||
end
|
||||
|
||||
prices
|
||||
end
|
||||
prices
|
||||
end
|
||||
end
|
||||
|
||||
def build_holdings_for_date(date)
|
||||
|
|
|
@ -176,12 +176,12 @@ class Demo::Generator
|
|||
|
||||
def load_securities!
|
||||
# Create an unknown security to simulate edge cases
|
||||
Security.create! ticker: "UNKNOWN", name: "Unknown Demo Stock"
|
||||
Security.create! ticker: "UNKNOWN", name: "Unknown Demo Stock", exchange_mic: "UNKNOWN"
|
||||
|
||||
securities = [
|
||||
{ ticker: "AAPL", name: "Apple Inc.", reference_price: 210 },
|
||||
{ ticker: "TM", name: "Toyota Motor Corporation", reference_price: 202 },
|
||||
{ ticker: "MSFT", name: "Microsoft Corporation", reference_price: 455 }
|
||||
{ ticker: "AAPL", exchange_mic: "NASDAQ", name: "Apple Inc.", reference_price: 210 },
|
||||
{ ticker: "TM", exchange_mic: "NYSE", name: "Toyota Motor Corporation", reference_price: 202 },
|
||||
{ ticker: "MSFT", exchange_mic: "NASDAQ", name: "Microsoft Corporation", reference_price: 455 }
|
||||
]
|
||||
|
||||
securities.each do |security_attributes|
|
||||
|
@ -193,7 +193,7 @@ class Demo::Generator
|
|||
low_price = reference - 20
|
||||
high_price = reference + 20
|
||||
Security::Price.create! \
|
||||
ticker: security.ticker,
|
||||
security: security,
|
||||
date: date,
|
||||
price: Faker::Number.positive(from: low_price, to: high_price)
|
||||
end
|
||||
|
|
|
@ -42,9 +42,10 @@ class Provider::Synth
|
|||
)
|
||||
end
|
||||
|
||||
def fetch_security_prices(ticker:, start_date:, end_date:)
|
||||
def fetch_security_prices(ticker:, mic_code:, start_date:, end_date:)
|
||||
prices = paginate(
|
||||
"#{base_url}/tickers/#{ticker}/open-close",
|
||||
mic_code: mic_code,
|
||||
start_date: start_date,
|
||||
end_date: end_date
|
||||
) do |body|
|
||||
|
|
|
@ -2,6 +2,7 @@ class Security < ApplicationRecord
|
|||
before_save :upcase_ticker
|
||||
|
||||
has_many :trades, dependent: :nullify, class_name: "Account::Trade"
|
||||
has_many :prices, dependent: :destroy
|
||||
|
||||
validates :ticker, presence: true
|
||||
validates :ticker, uniqueness: { scope: :exchange_mic, case_sensitive: false }
|
||||
|
@ -26,7 +27,7 @@ class Security < ApplicationRecord
|
|||
}
|
||||
|
||||
def current_price
|
||||
@current_price ||= Security::Price.find_price(ticker:, date: Date.current)
|
||||
@current_price ||= Security::Price.find_price(security: self, date: Date.current)
|
||||
return nil if @current_price.nil?
|
||||
Money.new(@current_price.price, @current_price.currency)
|
||||
end
|
||||
|
|
|
@ -1,33 +1,31 @@
|
|||
class Security::Price < ApplicationRecord
|
||||
include Provided
|
||||
|
||||
before_save :upcase_ticker
|
||||
|
||||
validates :ticker, presence: true, uniqueness: { scope: :date, case_sensitive: false }
|
||||
belongs_to :security
|
||||
|
||||
class << self
|
||||
def find_price(ticker:, date:, cache: true)
|
||||
result = find_by(ticker:, date:)
|
||||
def find_price(security:, date:, cache: true)
|
||||
result = find_by(security:, date:)
|
||||
|
||||
result || fetch_price_from_provider(ticker:, date:, cache:)
|
||||
result || fetch_price_from_provider(security:, date:, cache:)
|
||||
end
|
||||
|
||||
def find_prices(ticker:, start_date:, end_date: Date.current, cache: true)
|
||||
prices = where(ticker:, date: start_date..end_date).to_a
|
||||
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(ticker:, start_date: missing_dates.first, end_date: missing_dates.last, cache:)
|
||||
prices += fetch_prices_from_provider(
|
||||
security: security,
|
||||
start_date: missing_dates.first,
|
||||
end_date: missing_dates.last,
|
||||
cache: cache
|
||||
)
|
||||
end
|
||||
|
||||
prices
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def upcase_ticker
|
||||
self.ticker = ticker.upcase
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,17 +6,18 @@ module Security::Price::Provided
|
|||
class_methods do
|
||||
private
|
||||
|
||||
def fetch_price_from_provider(ticker:, date:, cache: false)
|
||||
def fetch_price_from_provider(security:, date:, cache: false)
|
||||
return nil unless security_prices_provider.present?
|
||||
|
||||
response = security_prices_provider.fetch_security_prices \
|
||||
ticker: ticker,
|
||||
ticker: security.ticker,
|
||||
mic_code: security.exchange_mic,
|
||||
start_date: date,
|
||||
end_date: date
|
||||
|
||||
if response.success? && response.prices.size > 0
|
||||
price = Security::Price.new \
|
||||
ticker: ticker,
|
||||
security: security,
|
||||
date: response.prices.first[:date],
|
||||
price: response.prices.first[:price],
|
||||
currency: response.prices.first[:currency]
|
||||
|
@ -28,18 +29,20 @@ module Security::Price::Provided
|
|||
end
|
||||
end
|
||||
|
||||
def fetch_prices_from_provider(ticker:, start_date:, end_date:, cache: false)
|
||||
def fetch_prices_from_provider(security:, start_date:, end_date:, cache: false)
|
||||
return [] unless security_prices_provider.present?
|
||||
return [] unless security
|
||||
|
||||
response = security_prices_provider.fetch_security_prices \
|
||||
ticker: ticker,
|
||||
ticker: security.ticker,
|
||||
mic_code: security.exchange_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(
|
||||
ticker: ticker,
|
||||
security: security,
|
||||
date: price[:date]
|
||||
) do |p|
|
||||
p.price = price[:price]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue