1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-18 20:59:39 +02:00

First pass at security price reference (#1388)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions

* 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:
Josh Pigford 2024-10-29 15:37:59 -04:00 committed by GitHub
parent bf695972e4
commit 490f44589e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 155 additions and 88 deletions

View file

@ -38,23 +38,31 @@ class Account::Holding::Syncer
def security_prices def security_prices
@security_prices ||= begin @security_prices ||= begin
prices = {} prices = {}
ticker_start_dates = {} ticker_securities = {}
sync_entries.each do |entry| sync_entries.each do |entry|
unless ticker_start_dates[entry.account_trade.security.ticker] security = entry.account_trade.security
ticker_start_dates[entry.account_trade.security.ticker] = entry.date unless ticker_securities[security.ticker]
end ticker_securities[security.ticker] = {
end security: security,
start_date: entry.date
}
end
end
ticker_start_dates.each do |ticker, date| ticker_securities.each do |ticker, data|
fetched_prices = Security::Price.find_prices(ticker: ticker, start_date: date, end_date: Date.current) fetched_prices = Security::Price.find_prices(
gapfilled_prices = Gapfiller.new(fetched_prices, start_date: date, end_date: Date.current, cache: false).run security: data[:security],
prices[ticker] = gapfilled_prices start_date: data[:start_date],
end 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 prices
end end
end end
def build_holdings_for_date(date) def build_holdings_for_date(date)

View file

@ -176,12 +176,12 @@ class Demo::Generator
def load_securities! def load_securities!
# Create an unknown security to simulate edge cases # 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 = [ securities = [
{ ticker: "AAPL", name: "Apple Inc.", reference_price: 210 }, { ticker: "AAPL", exchange_mic: "NASDAQ", name: "Apple Inc.", reference_price: 210 },
{ ticker: "TM", name: "Toyota Motor Corporation", reference_price: 202 }, { ticker: "TM", exchange_mic: "NYSE", name: "Toyota Motor Corporation", reference_price: 202 },
{ ticker: "MSFT", name: "Microsoft Corporation", reference_price: 455 } { ticker: "MSFT", exchange_mic: "NASDAQ", name: "Microsoft Corporation", reference_price: 455 }
] ]
securities.each do |security_attributes| securities.each do |security_attributes|
@ -193,7 +193,7 @@ class Demo::Generator
low_price = reference - 20 low_price = reference - 20
high_price = reference + 20 high_price = reference + 20
Security::Price.create! \ Security::Price.create! \
ticker: security.ticker, security: security,
date: date, date: date,
price: Faker::Number.positive(from: low_price, to: high_price) price: Faker::Number.positive(from: low_price, to: high_price)
end end

View file

@ -42,9 +42,10 @@ class Provider::Synth
) )
end end
def fetch_security_prices(ticker:, start_date:, end_date:) def fetch_security_prices(ticker:, mic_code:, start_date:, end_date:)
prices = paginate( prices = paginate(
"#{base_url}/tickers/#{ticker}/open-close", "#{base_url}/tickers/#{ticker}/open-close",
mic_code: mic_code,
start_date: start_date, start_date: start_date,
end_date: end_date end_date: end_date
) do |body| ) do |body|

View file

@ -2,6 +2,7 @@ class Security < ApplicationRecord
before_save :upcase_ticker before_save :upcase_ticker
has_many :trades, dependent: :nullify, class_name: "Account::Trade" has_many :trades, dependent: :nullify, class_name: "Account::Trade"
has_many :prices, dependent: :destroy
validates :ticker, presence: true validates :ticker, presence: true
validates :ticker, uniqueness: { scope: :exchange_mic, case_sensitive: false } validates :ticker, uniqueness: { scope: :exchange_mic, case_sensitive: false }
@ -26,7 +27,7 @@ class Security < ApplicationRecord
} }
def current_price 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? return nil if @current_price.nil?
Money.new(@current_price.price, @current_price.currency) Money.new(@current_price.price, @current_price.currency)
end end

View file

@ -1,33 +1,31 @@
class Security::Price < ApplicationRecord class Security::Price < ApplicationRecord
include Provided include Provided
before_save :upcase_ticker belongs_to :security
validates :ticker, presence: true, uniqueness: { scope: :date, case_sensitive: false }
class << self class << self
def find_price(ticker:, date:, cache: true) def find_price(security:, date:, cache: true)
result = find_by(ticker:, date:) result = find_by(security:, date:)
result || fetch_price_from_provider(ticker:, date:, cache:) result || fetch_price_from_provider(security:, date:, cache:)
end end
def find_prices(ticker:, start_date:, end_date: Date.current, cache: true) def find_prices(security:, start_date:, end_date: Date.current, cache: true)
prices = where(ticker:, date: start_date..end_date).to_a prices = where(security_id: security.id, date: start_date..end_date).to_a
all_dates = (start_date..end_date).to_a.to_set all_dates = (start_date..end_date).to_a.to_set
existing_dates = prices.map(&:date).to_set existing_dates = prices.map(&:date).to_set
missing_dates = (all_dates - existing_dates).sort missing_dates = (all_dates - existing_dates).sort
if missing_dates.any? 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 end
prices prices
end end
end end
private
def upcase_ticker
self.ticker = ticker.upcase
end
end end

View file

@ -6,17 +6,18 @@ module Security::Price::Provided
class_methods do class_methods do
private 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? return nil unless security_prices_provider.present?
response = security_prices_provider.fetch_security_prices \ response = security_prices_provider.fetch_security_prices \
ticker: ticker, ticker: security.ticker,
mic_code: security.exchange_mic,
start_date: date, start_date: date,
end_date: date end_date: date
if response.success? && response.prices.size > 0 if response.success? && response.prices.size > 0
price = Security::Price.new \ price = Security::Price.new \
ticker: ticker, security: security,
date: response.prices.first[:date], date: response.prices.first[:date],
price: response.prices.first[:price], price: response.prices.first[:price],
currency: response.prices.first[:currency] currency: response.prices.first[:currency]
@ -28,18 +29,20 @@ module Security::Price::Provided
end end
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_prices_provider.present?
return [] unless security
response = security_prices_provider.fetch_security_prices \ response = security_prices_provider.fetch_security_prices \
ticker: ticker, ticker: security.ticker,
mic_code: security.exchange_mic,
start_date: start_date, start_date: start_date,
end_date: end_date end_date: end_date
if response.success? if response.success?
response.prices.map do |price| response.prices.map do |price|
new_price = Security::Price.find_or_initialize_by( new_price = Security::Price.find_or_initialize_by(
ticker: ticker, security: security,
date: price[:date] date: price[:date]
) do |p| ) do |p|
p.price = price[:price] p.price = price[:price]

View file

@ -0,0 +1,14 @@
class AddReferenceToSecurityPrices < ActiveRecord::Migration[7.2]
def change
add_reference :security_prices, :security, foreign_key: true, type: :uuid
reversible do |dir|
dir.up do
Security::Price.find_each do |sp|
security = Security.find_by(ticker: sp.ticker)
sp.update_column(:security_id, security&.id)
end
end
end
end
end

3
db/schema.rb generated
View file

@ -494,6 +494,8 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_29_184115) do
t.string "currency", default: "USD" t.string "currency", default: "USD"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.uuid "security_id"
t.index ["security_id"], name: "index_security_prices_on_security_id"
end end
create_table "sessions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| create_table "sessions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
@ -606,6 +608,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_29_184115) do
add_foreign_key "imports", "families" add_foreign_key "imports", "families"
add_foreign_key "institutions", "families" add_foreign_key "institutions", "families"
add_foreign_key "merchants", "families" add_foreign_key "merchants", "families"
add_foreign_key "security_prices", "securities"
add_foreign_key "sessions", "impersonation_sessions", column: "active_impersonator_session_id" add_foreign_key "sessions", "impersonation_sessions", column: "active_impersonator_session_id"
add_foreign_key "sessions", "users" add_foreign_key "sessions", "users"
add_foreign_key "taggings", "tags" add_foreign_key "taggings", "tags"

View file

@ -1,11 +1,11 @@
one: one:
ticker: AAPL security: aapl
date: <%= Date.current %> date: <%= Date.current %>
price: 215 price: 215
currency: USD currency: USD
two: two:
ticker: AAPL security: aapl
date: <%= 1.day.ago.to_date %> date: <%= 1.day.ago.to_date %>
price: 214 price: 214
currency: USD currency: USD

View file

@ -10,7 +10,12 @@ module SecurityPriceProviderInterfaceTest
test "security price provider response contract" do test "security price provider response contract" do
VCR.use_cassette "synth/security_prices" do VCR.use_cassette "synth/security_prices" do
response = @subject.fetch_security_prices ticker: "AAPL", start_date: Date.iso8601("2024-01-01"), end_date: Date.iso8601("2024-08-01") response = @subject.fetch_security_prices(
ticker: "AAPL",
mic_code: "XNAS",
start_date: Date.iso8601("2024-01-01"),
end_date: Date.iso8601("2024-08-01")
)
assert_respond_to response, :prices assert_respond_to response, :prices
assert_respond_to response, :success? assert_respond_to response, :success?

View file

@ -14,6 +14,7 @@ class Account::Holding::SyncerTest < ActiveSupport::TestCase
end end
test "can buy and sell securities" do test "can buy and sell securities" do
# First create securities with their prices
security1 = create_security("AMZN", prices: [ security1 = create_security("AMZN", prices: [
{ date: 2.days.ago.to_date, price: 214 }, { date: 2.days.ago.to_date, price: 214 },
{ date: 1.day.ago.to_date, price: 215 }, { date: 1.day.ago.to_date, price: 215 },
@ -25,19 +26,18 @@ class Account::Holding::SyncerTest < ActiveSupport::TestCase
{ date: Date.current, price: 124 } { date: Date.current, price: 124 }
]) ])
# Then create trades after prices exist
create_trade(security1, account: @account, qty: 10, date: 2.days.ago.to_date) # buy 10 shares of AMZN create_trade(security1, account: @account, qty: 10, date: 2.days.ago.to_date) # buy 10 shares of AMZN
create_trade(security1, account: @account, qty: 2, date: 1.day.ago.to_date) # buy 2 shares of AMZN create_trade(security1, account: @account, qty: 2, date: 1.day.ago.to_date) # buy 2 shares of AMZN
create_trade(security2, account: @account, qty: 20, date: 1.day.ago.to_date) # buy 20 shares of NVDA create_trade(security2, account: @account, qty: 20, date: 1.day.ago.to_date) # buy 20 shares of NVDA
create_trade(security1, account: @account, qty: -10, date: Date.current) # sell 10 shares of AMZN create_trade(security1, account: @account, qty: -10, date: Date.current) # sell 10 shares of AMZN
expected = [ expected = [
{ ticker: "AMZN", qty: 10, price: 214, amount: 10 * 214, date: 2.days.ago.to_date }, { security: security1, qty: 10, price: 214, amount: 10 * 214, date: 2.days.ago.to_date },
{ ticker: "AMZN", qty: 12, price: 215, amount: 12 * 215, date: 1.day.ago.to_date }, { security: security1, qty: 12, price: 215, amount: 12 * 215, date: 1.day.ago.to_date },
{ ticker: "AMZN", qty: 2, price: 216, amount: 2 * 216, date: Date.current }, { security: security1, qty: 2, price: 216, amount: 2 * 216, date: Date.current },
{ ticker: "NVDA", qty: 20, price: 122, amount: 20 * 122, date: 1.day.ago.to_date }, { security: security2, qty: 20, price: 122, amount: 20 * 122, date: 1.day.ago.to_date },
{ ticker: "NVDA", qty: 20, price: 124, amount: 20 * 124, date: Date.current } { security: security2, qty: 20, price: 124, amount: 20 * 124, date: Date.current }
] ]
run_sync_for(@account) run_sync_for(@account)
@ -55,7 +55,7 @@ class Account::Holding::SyncerTest < ActiveSupport::TestCase
create_trade(amzn, account: @account, qty: 10, date: Date.current, price: 215) create_trade(amzn, account: @account, qty: 10, date: Date.current, price: 215)
expected = [ expected = [
{ ticker: "AMZN", qty: 10, price: 215, amount: 10 * 215, date: Date.current } { security: amzn, qty: 10, price: 215, amount: 10 * 215, date: Date.current }
] ]
run_sync_for(@account) run_sync_for(@account)
@ -72,16 +72,16 @@ class Account::Holding::SyncerTest < ActiveSupport::TestCase
# 1 day ago — finds daily price, uses it # 1 day ago — finds daily price, uses it
# Today — no daily price, no entry, so price and amount are `nil` # Today — no daily price, no entry, so price and amount are `nil`
expected = [ expected = [
{ ticker: "AMZN", qty: 10, price: 210, amount: 10 * 210, date: 2.days.ago.to_date }, { security: amzn, qty: 10, price: 210, amount: 10 * 210, date: 2.days.ago.to_date },
{ ticker: "AMZN", qty: 10, price: 215, amount: 10 * 215, date: 1.day.ago.to_date }, { security: amzn, qty: 10, price: 215, amount: 10 * 215, date: 1.day.ago.to_date },
{ ticker: "AMZN", qty: 10, price: nil, amount: nil, date: Date.current } { security: amzn, qty: 10, price: nil, amount: nil, date: Date.current }
] ]
fetched_prices = [ Security::Price.new(ticker: "AMZN", date: 1.day.ago.to_date, price: 215) ] fetched_prices = [ Security::Price.new(ticker: "AMZN", date: 1.day.ago.to_date, price: 215) ]
Gapfiller.any_instance.expects(:run).returns(fetched_prices) Gapfiller.any_instance.expects(:run).returns(fetched_prices)
Security::Price.expects(:find_prices) Security::Price.expects(:find_prices)
.with(start_date: 2.days.ago.to_date, end_date: Date.current, ticker: "AMZN") .with(security: amzn, start_date: 2.days.ago.to_date, end_date: Date.current)
.once .once
.returns(fetched_prices) .returns(fetched_prices)
@ -103,13 +103,13 @@ class Account::Holding::SyncerTest < ActiveSupport::TestCase
{ date: monday, price: 220 } { date: monday, price: 220 }
]) ])
create_trade(tm, account: @account, qty: 10, date: friday) create_trade(tm, account: @account, qty: 10, date: friday, price: 210)
expected = [ expected = [
{ ticker: "TM", qty: 10, price: 210, amount: 10 * 210, date: friday }, { security: tm, qty: 10, price: 210, amount: 10 * 210, date: friday },
{ ticker: "TM", qty: 10, price: 210, amount: 10 * 210, date: saturday }, { security: tm, qty: 10, price: 210, amount: 10 * 210, date: saturday },
{ ticker: "TM", qty: 10, price: 210, amount: 10 * 210, date: sunday }, { security: tm, qty: 10, price: 210, amount: 10 * 210, date: sunday },
{ ticker: "TM", qty: 10, price: 220, amount: 10 * 220, date: monday } { security: tm, qty: 10, price: 220, amount: 10 * 220, date: monday }
] ]
run_sync_for(@account) run_sync_for(@account)
@ -122,12 +122,15 @@ class Account::Holding::SyncerTest < ActiveSupport::TestCase
def assert_holdings(expected_holdings) def assert_holdings(expected_holdings)
holdings = @account.holdings.includes(:security).to_a holdings = @account.holdings.includes(:security).to_a
expected_holdings.each do |expected_holding| expected_holdings.each do |expected_holding|
actual_holding = holdings.find { |holding| holding.security.ticker == expected_holding[:ticker] && holding.date == expected_holding[:date] } actual_holding = holdings.find { |holding|
holding.security == expected_holding[:security] &&
holding.date == expected_holding[:date]
}
date = expected_holding[:date] date = expected_holding[:date]
expected_price = expected_holding[:price] expected_price = expected_holding[:price]
expected_qty = expected_holding[:qty] expected_qty = expected_holding[:qty]
expected_amount = expected_holding[:amount] expected_amount = expected_holding[:amount]
ticker = expected_holding[:ticker] ticker = expected_holding[:security].ticker
assert actual_holding, "expected #{ticker} holding on date: #{date}" assert actual_holding, "expected #{ticker} holding on date: #{date}"
assert_equal expected_holding[:qty], actual_holding.qty, "expected #{expected_qty} qty for holding #{ticker} on date: #{date}" assert_equal expected_holding[:qty], actual_holding.qty, "expected #{expected_qty} qty for holding #{ticker} on date: #{date}"

View file

@ -58,7 +58,7 @@ class Account::HoldingTest < ActiveSupport::TestCase
end end
def create_holding(security, date, qty) def create_holding(security, date, qty)
price = Security::Price.find_by(date: date, ticker: security.ticker).price price = Security::Price.find_by(date: date, security: security).price
@account.holdings.create! \ @account.holdings.create! \
date: date, date: date,

View file

@ -10,7 +10,12 @@ class Provider::SynthTest < ActiveSupport::TestCase
test "fetches paginated securities prices" do test "fetches paginated securities prices" do
VCR.use_cassette("synth/security_prices") do VCR.use_cassette("synth/security_prices") do
response = @synth.fetch_security_prices ticker: "AAPL", start_date: Date.iso8601("2024-01-01"), end_date: Date.iso8601("2024-08-01") response = @synth.fetch_security_prices(
ticker: "AAPL",
mic_code: "XNAS",
start_date: Date.iso8601("2024-01-01"),
end_date: Date.iso8601("2024-08-01")
)
assert 213, response.size assert 213, response.size
end end

View file

@ -18,54 +18,63 @@ class Security::PriceTest < ActiveSupport::TestCase
test "finds single security price in DB" do test "finds single security price in DB" do
@provider.expects(:fetch_security_prices).never @provider.expects(:fetch_security_prices).never
security = securities(:aapl)
price = security_prices(:one) price = security_prices(:one)
assert_equal price, Security::Price.find_price(ticker: price.ticker, date: price.date) assert_equal price, Security::Price.find_price(security: security, date: price.date)
end end
test "caches prices to DB" do test "caches prices to DB" do
expected_price = 314.34 expected_price = 314.34
@provider.expects(:fetch_security_prices) security = securities(:aapl)
.once tomorrow = Date.current + 1.day
.returns(
OpenStruct.new(
success?: true,
prices: [ { date: Date.current, price: expected_price } ]
)
)
fetched_rate = Security::Price.find_price(ticker: "NVDA", date: Date.current, cache: true) @provider.expects(:fetch_security_prices)
refetched_rate = Security::Price.find_price(ticker: "NVDA", date: Date.current, cache: true) .with(ticker: security.ticker, mic_code: security.exchange_mic, start_date: tomorrow, end_date: tomorrow)
.once
.returns(
OpenStruct.new(
success?: true,
prices: [ { date: tomorrow, price: expected_price } ]
)
)
fetched_rate = Security::Price.find_price(security: security, date: tomorrow, cache: true)
refetched_rate = Security::Price.find_price(security: security, date: tomorrow, cache: true)
assert_equal expected_price, fetched_rate.price assert_equal expected_price, fetched_rate.price
assert_equal expected_price, refetched_rate.price assert_equal expected_price, refetched_rate.price
end end
test "returns nil if no price found in DB or from provider" do test "returns nil if no price found in DB or from provider" do
security = securities(:aapl)
Security::Price.delete_all # Clear any existing prices
@provider.expects(:fetch_security_prices) @provider.expects(:fetch_security_prices)
.with(ticker: "NVDA", start_date: Date.current, end_date: Date.current) .with(ticker: security.ticker, mic_code: security.exchange_mic, start_date: Date.current, end_date: Date.current)
.once .once
.returns(OpenStruct.new(success?: false)) .returns(OpenStruct.new(success?: false))
assert_not Security::Price.find_price(ticker: "NVDA", date: Date.current) assert_not Security::Price.find_price(security: security, date: Date.current)
end end
test "returns nil if price not found in DB and provider disabled" do test "returns nil if price not found in DB and provider disabled" do
Security::Price.unstub(:security_prices_provider) Security::Price.unstub(:security_prices_provider)
security = Security.new(ticker: "NVDA")
with_env_overrides SYNTH_API_KEY: nil do with_env_overrides SYNTH_API_KEY: nil do
assert_not Security::Price.find_price(ticker: "NVDA", date: Date.current) assert_not Security::Price.find_price(security: security, date: Date.current)
end end
end end
test "fetches multiple dates at once" do test "fetches multiple dates at once" do
@provider.expects(:fetch_security_prices).never @provider.expects(:fetch_security_prices).never
security = securities(:aapl)
price1 = security_prices(:one) # AAPL today price1 = security_prices(:one) # AAPL today
price2 = security_prices(:two) # AAPL yesterday price2 = security_prices(:two) # AAPL yesterday
fetched_prices = Security::Price.find_prices(start_date: 1.day.ago.to_date, end_date: Date.current, ticker: "AAPL").sort_by(&:date) fetched_prices = Security::Price.find_prices(security: security, start_date: 1.day.ago.to_date, end_date: Date.current).sort_by(&:date)
assert_equal price1, fetched_prices[1] assert_equal price1, fetched_prices[1]
assert_equal price2, fetched_prices[0] assert_equal price2, fetched_prices[0]
@ -73,26 +82,33 @@ class Security::PriceTest < ActiveSupport::TestCase
test "caches multiple prices to DB" do test "caches multiple prices to DB" do
missing_price = 213.21 missing_price = 213.21
security = securities(:aapl)
@provider.expects(:fetch_security_prices) @provider.expects(:fetch_security_prices)
.with(ticker: "AAPL", start_date: 2.days.ago.to_date, end_date: 2.days.ago.to_date) .with(ticker: security.ticker,
mic_code: security.exchange_mic,
start_date: 2.days.ago.to_date,
end_date: 2.days.ago.to_date)
.returns(OpenStruct.new(success?: true, prices: [ { date: 2.days.ago.to_date, price: missing_price } ])) .returns(OpenStruct.new(success?: true, prices: [ { date: 2.days.ago.to_date, price: missing_price } ]))
.once .once
price1 = security_prices(:one) # AAPL today price1 = security_prices(:one) # AAPL today
price2 = security_prices(:two) # AAPL yesterday price2 = security_prices(:two) # AAPL yesterday
fetched_prices = Security::Price.find_prices(ticker: "AAPL", start_date: 2.days.ago.to_date, end_date: Date.current, cache: true) fetched_prices = Security::Price.find_prices(security: security, start_date: 2.days.ago.to_date, end_date: Date.current, cache: true)
refetched_prices = Security::Price.find_prices(ticker: "AAPL", start_date: 2.days.ago.to_date, end_date: Date.current, cache: true) refetched_prices = Security::Price.find_prices(security: security, start_date: 2.days.ago.to_date, end_date: Date.current, cache: true)
assert_equal [ missing_price, price2.price, price1.price ], fetched_prices.sort_by(&:date).map(&:price) assert_equal [ missing_price, price2.price, price1.price ], fetched_prices.sort_by(&:date).map(&:price)
assert_equal [ missing_price, price2.price, price1.price ], refetched_prices.sort_by(&:date).map(&:price) assert_equal [ missing_price, price2.price, price1.price ], refetched_prices.sort_by(&:date).map(&:price)
assert Security::Price.exists?(security: security, date: 2.days.ago.to_date, price: missing_price)
end end
test "returns empty array if no prices found in DB or from provider" do test "returns empty array if no prices found in DB or from provider" do
Security::Price.unstub(:security_prices_provider) Security::Price.unstub(:security_prices_provider)
with_env_overrides SYNTH_API_KEY: nil do with_env_overrides SYNTH_API_KEY: nil do
assert_equal [], Security::Price.find_prices(ticker: "NVDA", start_date: 10.days.ago.to_date, end_date: Date.current) assert_equal [], Security::Price.find_prices(security: Security.new(ticker: "NVDA"), start_date: 10.days.ago.to_date, end_date: Date.current)
end end
end end
end end

View file

@ -29,7 +29,7 @@ module Account::EntriesTestHelper
end end
def create_trade(security, account:, qty:, date:, price: nil) def create_trade(security, account:, qty:, date:, price: nil)
trade_price = price || Security::Price.find_by!(ticker: security.ticker, date: date).price trade_price = price || Security::Price.find_by!(security: security, date: date).price
trade = Account::Trade.new \ trade = Account::Trade.new \
qty: qty, qty: qty,

View file

@ -1,9 +1,19 @@
module SecuritiesTestHelper module SecuritiesTestHelper
def create_security(ticker, prices:) def create_security(ticker, prices:)
security = Security.create!(
ticker: ticker,
exchange_mic: "XNAS"
)
prices.each do |price| prices.each do |price|
Security::Price.create! ticker: ticker, date: price[:date], price: price[:price] Security::Price.create!(
security: security,
date: price[:date],
price: price[:price],
currency: "USD"
)
end end
Security.create! ticker: ticker security
end end
end end