1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-03 04:25:21 +02:00

Enhance security information retrieval and handling (#1826)

* Enhance security information retrieval and handling

- Add support for operating MIC codes in security info fetching
- Update security uniqueness validation to handle unknown securities
- Improve security creation and update logic in Plaid investment sync
- Update combobox and view components to handle operating MIC codes
- Add unknown flag for securities with incomplete information

* Update schema.rb

* Refactor the need for mic codes

* Don't fetch prices unless a security has the necessary mic code

* Deduplication

* Lint

* Update Securities and Plaid Investment Sync

- Modify PlaidInvestmentSync to return plaid_security for USD cash
- Add non-null constraint to Securities ticker column
- Update Securities fixture to use exchange_operating_mic instead of exchange_mic

---------

Signed-off-by: Josh Pigford <josh@joshpigford.com>
This commit is contained in:
Josh Pigford 2025-02-11 10:40:30 -06:00 committed by GitHub
parent fb6c6fa6bb
commit 68d7cb5de6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 203 additions and 31 deletions

View file

@ -111,13 +111,10 @@ class Account::TradeBuilder
end
def security
ticker_symbol, exchange_mic, exchange_acronym, exchange_country_code = ticker.split("|")
ticker_symbol, exchange_operating_mic = ticker.split("|")
security = Security.find_or_create_by(ticker: ticker_symbol, exchange_mic: exchange_mic, country_code: exchange_country_code)
security.update(exchange_acronym: exchange_acronym)
FetchSecurityInfoJob.perform_later(security.id)
security
Security.find_or_create_by(ticker: ticker_symbol, exchange_operating_mic: exchange_operating_mic) do |s|
FetchSecurityInfoJob.perform_later(s.id)
end
end
end

View file

@ -231,12 +231,12 @@ class Demo::Generator
def load_securities!
# Create an unknown security to simulate edge cases
Security.create! ticker: "UNKNOWN", name: "Unknown Demo Stock", exchange_mic: "UNKNOWN"
Security.create! ticker: "UNKNOWN", name: "Unknown Demo Stock"
securities = [
{ 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 }
{ ticker: "AAPL", exchange_mic: "XNGS", exchange_operating_mic: "XNAS", name: "Apple Inc.", reference_price: 210 },
{ ticker: "TM", exchange_mic: "XNYS", exchange_operating_mic: "XNYS", name: "Toyota Motor Corporation", reference_price: 202 },
{ ticker: "MSFT", exchange_mic: "XNGS", exchange_operating_mic: "XNAS", name: "Microsoft Corporation", reference_price: 455 }
]
securities.each do |security_attributes|

View file

@ -82,12 +82,15 @@ class PlaidInvestmentSync
end
return [ nil, nil ] if plaid_security.nil? || plaid_security.ticker_symbol.blank?
return [ nil, plaid_security ] if plaid_security.ticker_symbol == "CUR:USD" # internally, we do not consider cash a security and track it separately
operating_mic = plaid_security.market_identifier_code
# Find any matching security
security = Security.find_or_create_by!(
ticker: plaid_security.ticker_symbol,
exchange_mic: plaid_security.market_identifier_code || "XNAS",
country_code: "US"
) unless plaid_security.ticker_symbol == "CUR:USD" # internally, we do not consider cash a security and track it separately
exchange_operating_mic: operating_mic
)
[ security, plaid_security ]
end

View file

@ -145,6 +145,7 @@ class Provider::Synth
logo_url: security.dig("logo_url"),
exchange_acronym: security.dig("exchange", "acronym"),
exchange_mic: security.dig("exchange", "mic_code"),
exchange_operating_mic: security.dig("exchange", "operating_mic_code"),
country_code: security.dig("exchange", "country_code")
}
end
@ -155,9 +156,10 @@ class Provider::Synth
raw_response: response
end
def fetch_security_info(ticker:, mic_code:)
def fetch_security_info(ticker:, mic_code: nil, operating_mic: nil)
response = client.get("#{base_url}/tickers/#{ticker}") do |req|
req.params["mic_code"] = mic_code
req.params["mic_code"] = mic_code if mic_code.present?
req.params["operating_mic"] = operating_mic if operating_mic.present?
end
parsed = JSON.parse(response.body)

View file

@ -6,7 +6,7 @@ class Security < ApplicationRecord
has_many :prices, dependent: :destroy
validates :ticker, presence: true
validates :ticker, uniqueness: { scope: :exchange_mic, case_sensitive: false }
validates :ticker, uniqueness: { scope: :exchange_operating_mic, case_sensitive: false }
class << self
def search(query)
@ -30,11 +30,15 @@ class Security < ApplicationRecord
name: name,
logo_url: logo_url,
exchange_acronym: exchange_acronym,
exchange_mic: exchange_mic,
exchange_operating_mic: exchange_operating_mic,
exchange_country_code: country_code
)
end
def has_prices?
exchange_operating_mic.present?
end
private
def upcase_ticker

View file

@ -8,6 +8,7 @@ module Security::Price::Provided
def fetch_price_from_provider(security:, date:, cache: false)
return nil unless security_prices_provider.present?
return nil unless security.has_prices?
response = security_prices_provider.fetch_security_prices \
ticker: security.ticker,
@ -32,6 +33,7 @@ module Security::Price::Provided
def fetch_prices_from_provider(security:, start_date:, end_date:, cache: false)
return [] unless security_prices_provider.present?
return [] unless security
return [] unless security.has_prices?
response = security_prices_provider.fetch_security_prices \
ticker: security.ticker,

View file

@ -1,13 +1,14 @@
class Security::SynthComboboxOption
include ActiveModel::Model
attr_accessor :symbol, :name, :logo_url, :exchange_acronym, :exchange_mic, :exchange_country_code
attr_accessor :symbol, :name, :logo_url, :exchange_acronym, :exchange_country_code, :exchange_operating_mic
def id
"#{symbol}|#{exchange_mic}|#{exchange_acronym}|#{exchange_country_code}" # submitted by combobox as value
"#{symbol}|#{exchange_operating_mic}" # submitted by combobox as value
end
def to_combobox_display
"#{symbol} - #{name} (#{exchange_acronym})" # shown in combobox input when selected
display_code = exchange_acronym.presence || exchange_operating_mic
"#{symbol} - #{name} (#{display_code})" # shown in combobox input when selected
end
end