1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-02 12:05:19 +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

@ -2,70 +2,84 @@ require "test_helper"
require "ostruct"
class MarketDataSyncerTest < ActiveSupport::TestCase
include EntriesTestHelper, ProviderTestHelper
include ProviderTestHelper
test "syncs exchange rates with upsert" do
empty_db
SNAPSHOT_START_DATE = MarketDataSyncer::SNAPSHOT_DAYS.days.ago.to_date
PROVIDER_BUFFER = 5.days
family1 = Family.create!(name: "Family 1", currency: "USD")
account1 = family1.accounts.create!(name: "Account 1", currency: "USD", balance: 100, accountable: Depository.new)
account2 = family1.accounts.create!(name: "Account 2", currency: "CAD", balance: 100, accountable: Depository.new)
setup do
Security::Price.delete_all
ExchangeRate.delete_all
Trade.delete_all
Holding.delete_all
Security.delete_all
family2 = Family.create!(name: "Family 2", currency: "EUR")
account3 = family2.accounts.create!(name: "Account 3", currency: "EUR", balance: 100, accountable: Depository.new)
account4 = family2.accounts.create!(name: "Account 4", currency: "USD", balance: 100, accountable: Depository.new)
mock_provider = mock
Provider::Registry.any_instance.expects(:get_provider).with(:synth).returns(mock_provider).at_least_once
start_date = 1.month.ago.to_date
end_date = Date.current.in_time_zone("America/New_York").to_date
# Put an existing rate in DB to test upsert
ExchangeRate.create!(from_currency: "CAD", to_currency: "USD", date: start_date, rate: 2.0)
mock_provider.expects(:fetch_exchange_rates)
.with(from: "CAD", to: "USD", start_date: start_date, end_date: end_date)
.returns(provider_success_response([ OpenStruct.new(from: "CAD", to: "USD", date: start_date, rate: 1.0) ]))
mock_provider.expects(:fetch_exchange_rates)
.with(from: "USD", to: "EUR", start_date: start_date, end_date: end_date)
.returns(provider_success_response([ OpenStruct.new(from: "USD", to: "EUR", date: start_date, rate: 1.0) ]))
assert_difference "ExchangeRate.count", 1 do
MarketDataSyncer.new.sync_exchange_rates
end
assert_equal 1.0, ExchangeRate.where(from_currency: "CAD", to_currency: "USD", date: start_date).first.rate
@provider = mock("provider")
Provider::Registry.any_instance
.stubs(:get_provider)
.with(:synth)
.returns(@provider)
end
test "syncs security prices with upsert" do
empty_db
test "syncs required exchange rates" do
family = Family.create!(name: "Smith", currency: "USD")
family.accounts.create!(name: "Chequing",
currency: "CAD",
balance: 100,
accountable: Depository.new)
aapl = Security.create!(ticker: "AAPL", exchange_operating_mic: "XNAS")
# Seed stale rate so only the next missing day is fetched
ExchangeRate.create!(from_currency: "CAD",
to_currency: "USD",
date: SNAPSHOT_START_DATE,
rate: 2.0)
family = Family.create!(name: "Family 1", currency: "USD")
account = family.accounts.create!(name: "Account 1", currency: "USD", balance: 100, accountable: Investment.new)
expected_start_date = (SNAPSHOT_START_DATE + 1.day) - PROVIDER_BUFFER
end_date = Date.current.in_time_zone("America/New_York").to_date
mock_provider = mock
Provider::Registry.any_instance.expects(:get_provider).with(:synth).returns(mock_provider).at_least_once
@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: SNAPSHOT_START_DATE, rate: 1.5)
]))
start_date = 1.month.ago.to_date
end_date = Date.current.in_time_zone("America/New_York").to_date
before = ExchangeRate.count
MarketDataSyncer.new(mode: :snapshot).sync_exchange_rates
after = ExchangeRate.count
mock_provider.expects(:fetch_security_prices)
.with(aapl, start_date: start_date, end_date: end_date)
.returns(provider_success_response([ OpenStruct.new(security: aapl, date: start_date, price: 100, currency: "USD") ]))
assert_difference "Security::Price.count", 1 do
MarketDataSyncer.new.sync_prices
end
assert_operator after, :>, before, "Should insert at least one new exchange-rate row"
end
private
def empty_db
Invitation.destroy_all
Family.destroy_all
Security.destroy_all
end
test "syncs security prices" do
security = Security.create!(ticker: "AAPL", exchange_operating_mic: "XNAS")
expected_start_date = SNAPSHOT_START_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: SNAPSHOT_START_DATE,
price: 100,
currency: "USD")
]))
@provider.stubs(:fetch_security_info)
.with(symbol: "AAPL", exchange_operating_mic: "XNAS")
.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([]))
MarketDataSyncer.new(mode: :snapshot).sync_prices
assert_equal 1, Security::Price.where(security: security, date: SNAPSHOT_START_DATE).count
end
end