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

@ -1,52 +0,0 @@
require "test_helper"
require "ostruct"
class Account::ConvertibleTest < ActiveSupport::TestCase
include EntriesTestHelper, ProviderTestHelper
setup do
@family = families(:empty)
@family.update!(currency: "USD")
# Foreign account (currency is not in the family's primary currency, so it will require exchange rates for net worth rollups)
@account = @family.accounts.create!(name: "Test Account", currency: "EUR", balance: 10000, accountable: Depository.new)
@provider = mock
ExchangeRate.stubs(:provider).returns(@provider)
end
test "syncs required exchange rates for an account" do
create_valuation(account: @account, date: 1.day.ago.to_date, amount: 9500, currency: "EUR")
# Since we had a valuation 1 day ago, this account starts 2 days ago and needs daily exchange rates looking forward
assert_equal 2.days.ago.to_date, @account.start_date
ExchangeRate.delete_all
provider_response = provider_success_response(
[
OpenStruct.new(from: "EUR", to: "USD", date: 2.days.ago.to_date, rate: 1.1),
OpenStruct.new(from: "EUR", to: "USD", date: 1.day.ago.to_date, rate: 1.2),
OpenStruct.new(from: "EUR", to: "USD", date: Date.current, rate: 1.3)
]
)
@provider.expects(:fetch_exchange_rates)
.with(from: "EUR", to: "USD", start_date: 2.days.ago.to_date, end_date: Date.current)
.returns(provider_response)
assert_difference "ExchangeRate.count", 3 do
@account.sync_required_exchange_rates
end
end
test "does not sync rates for a domestic account" do
@account.update!(currency: "USD")
@provider.expects(:fetch_exchange_rates).never
assert_no_difference "ExchangeRate.count" do
@account.sync_required_exchange_rates
end
end
end

View file

@ -0,0 +1,107 @@
require "test_helper"
require "ostruct"
class Account::MarketDataSyncerTest < ActiveSupport::TestCase
include ProviderTestHelper
PROVIDER_BUFFER = 5.days
setup do
# Ensure a clean slate for deterministic assertions
Security::Price.delete_all
ExchangeRate.delete_all
Trade.delete_all
Holding.delete_all
Security.delete_all
Entry.delete_all
@provider = mock("provider")
Provider::Registry.any_instance
.stubs(:get_provider)
.with(:synth)
.returns(@provider)
end
test "syncs required exchange rates for a foreign-currency account" do
family = Family.create!(name: "Smith", currency: "USD")
account = family.accounts.create!(
name: "Chequing",
currency: "CAD",
balance: 100,
accountable: Depository.new
)
# Seed a rate for the first required day so that the syncer only needs the next day forward
existing_date = account.start_date
ExchangeRate.create!(from_currency: "CAD", to_currency: "USD", date: existing_date, rate: 2.0)
expected_start_date = (existing_date + 1.day) - PROVIDER_BUFFER
end_date = Date.current.in_time_zone("America/New_York").to_date
@provider.expects(:fetch_exchange_rates)
.with(from: "CAD",
to: "USD",
start_date: expected_start_date,
end_date: end_date)
.returns(provider_success_response([
OpenStruct.new(from: "CAD", to: "USD", date: existing_date, rate: 1.5)
]))
before = ExchangeRate.count
Account::MarketDataSyncer.new(account).sync_market_data
after = ExchangeRate.count
assert_operator after, :>, before, "Should insert at least one new exchange-rate row"
end
test "syncs security prices for securities traded by the account" do
family = Family.create!(name: "Smith", currency: "USD")
account = family.accounts.create!(
name: "Brokerage",
currency: "USD",
balance: 0,
accountable: Investment.new
)
security = Security.create!(ticker: "AAPL", exchange_operating_mic: "XNAS")
trade_date = 10.days.ago.to_date
trade = Trade.new(security: security, qty: 1, price: 100, currency: "USD")
account.entries.create!(
name: "Buy AAPL",
date: trade_date,
amount: 100,
currency: "USD",
entryable: trade
)
expected_start_date = trade_date - PROVIDER_BUFFER
end_date = Date.current.in_time_zone("America/New_York").to_date
@provider.expects(:fetch_security_prices)
.with(symbol: security.ticker,
exchange_operating_mic: security.exchange_operating_mic,
start_date: expected_start_date,
end_date: end_date)
.returns(provider_success_response([
OpenStruct.new(security: security,
date: trade_date,
price: 100,
currency: "USD")
]))
@provider.stubs(:fetch_security_info)
.with(symbol: security.ticker, exchange_operating_mic: security.exchange_operating_mic)
.returns(provider_success_response(OpenStruct.new(name: "Apple", logo_url: "logo")))
# Ignore exchange-rate calls for this test
@provider.stubs(:fetch_exchange_rates).returns(provider_success_response([]))
Account::MarketDataSyncer.new(account).sync_market_data
assert_equal 1, Security::Price.where(security: security, date: trade_date).count
end
end