1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 07:25:19 +02:00

Rename MarketDataSyncer to MarketDataImporter

This commit is contained in:
Zach Gollwitzer 2025-05-17 12:34:20 -04:00
parent b8903d0980
commit e75dbc978b
7 changed files with 100 additions and 100 deletions

View file

@ -15,6 +15,6 @@ class SyncMarketDataJob < ApplicationJob
mode = opts.fetch(:mode, :full) mode = opts.fetch(:mode, :full)
clear_cache = opts.fetch(:clear_cache, false) clear_cache = opts.fetch(:clear_cache, false)
MarketDataSyncer.new(mode: mode, clear_cache: clear_cache).sync MarketDataImporter.new(mode: mode, clear_cache: clear_cache).import_all
end end
end end

View file

@ -0,0 +1,82 @@
class Account::MarketDataImporter
attr_reader :account
def initialize(account)
@account = account
end
def import_all
import_exchange_rates
import_security_prices
end
def import_exchange_rates
return unless needs_exchange_rates?
return unless ExchangeRate.provider
pair_dates = {}
# 1. ENTRY-BASED PAIRS currencies that differ from the account currency
account.entries
.where.not(currency: account.currency)
.group(:currency)
.minimum(:date)
.each do |source_currency, date|
key = [ source_currency, account.currency ]
pair_dates[key] = [ pair_dates[key], date ].compact.min
end
# 2. ACCOUNT-BASED PAIR convert the account currency to the family currency (if different)
if foreign_account?
key = [ account.currency, account.family.currency ]
pair_dates[key] = [ pair_dates[key], account.start_date ].compact.min
end
pair_dates.each do |(source, target), start_date|
ExchangeRate.sync_provider_rates(
from: source,
to: target,
start_date: start_date,
end_date: Date.current
)
end
end
def import_security_prices
return unless Security.provider
account_securities = account.trades.map(&:security).uniq
return if account_securities.empty?
account_securities.each do |security|
security.sync_provider_prices(
start_date: first_required_price_date(security),
end_date: Date.current
)
security.sync_provider_details
end
end
private
# Calculates the first date we require a price for the given security scoped to this account
def first_required_price_date(security)
account.trades.with_entry
.where(security: security)
.where(entries: { account_id: account.id })
.minimum("entries.date")
end
def needs_exchange_rates?
has_multi_currency_entries? || foreign_account?
end
def has_multi_currency_entries?
account.entries.where.not(currency: account.currency).exists?
end
def foreign_account?
account.currency != account.family.currency
end
end

View file

@ -1,82 +0,0 @@
class Account::MarketDataSyncer
attr_reader :account
def initialize(account)
@account = account
end
def sync_market_data
sync_exchange_rates
sync_security_prices
end
private
def sync_exchange_rates
return unless needs_exchange_rates?
return unless ExchangeRate.provider
pair_dates = {}
# 1. ENTRY-BASED PAIRS currencies that differ from the account currency
account.entries
.where.not(currency: account.currency)
.group(:currency)
.minimum(:date)
.each do |source_currency, date|
key = [ source_currency, account.currency ]
pair_dates[key] = [ pair_dates[key], date ].compact.min
end
# 2. ACCOUNT-BASED PAIR convert the account currency to the family currency (if different)
if foreign_account?
key = [ account.currency, account.family.currency ]
pair_dates[key] = [ pair_dates[key], account.start_date ].compact.min
end
pair_dates.each do |(source, target), start_date|
ExchangeRate.sync_provider_rates(
from: source,
to: target,
start_date: start_date,
end_date: Date.current
)
end
end
def sync_security_prices
return unless Security.provider
account_securities = account.trades.map(&:security).uniq
return if account_securities.empty?
account_securities.each do |security|
security.sync_provider_prices(
start_date: first_required_price_date(security),
end_date: Date.current
)
security.sync_provider_details
end
end
# Calculates the first date we require a price for the given security scoped to this account
def first_required_price_date(security)
account.trades.with_entry
.where(security: security)
.where(entries: { account_id: account.id })
.minimum("entries.date")
end
def needs_exchange_rates?
has_multi_currency_entries? || foreign_account?
end
def has_multi_currency_entries?
account.entries.where.not(currency: account.currency).exists?
end
def foreign_account?
account.currency != account.family.currency
end
end

View file

@ -29,7 +29,7 @@ class Account::Syncer
# We rescue errors here because if this operation fails, we don't want to fail the entire sync since # We rescue errors here because if this operation fails, we don't want to fail the entire sync since
# we have reasonable fallbacks for missing market data. # we have reasonable fallbacks for missing market data.
def sync_market_data def sync_market_data
Account::MarketDataSyncer.new(account).sync_market_data Account::MarketDataImporter.new(account).import_all
rescue => e rescue => e
Rails.logger.error("Error syncing market data for account #{account.id}: #{e.message}") Rails.logger.error("Error syncing market data for account #{account.id}: #{e.message}")
Sentry.capture_exception(e) Sentry.capture_exception(e)

View file

@ -1,4 +1,4 @@
class MarketDataSyncer class MarketDataImporter
# By default, our graphs show 1M as the view, so by fetching 31 days, # By default, our graphs show 1M as the view, so by fetching 31 days,
# we ensure we can always show an accurate default graph # we ensure we can always show an accurate default graph
SNAPSHOT_DAYS = 31 SNAPSHOT_DAYS = 31
@ -10,15 +10,15 @@ class MarketDataSyncer
@clear_cache = clear_cache @clear_cache = clear_cache
end end
def sync def import_all
sync_prices import_security_prices
sync_exchange_rates import_exchange_rates
end end
# Syncs historical security prices (and details) # Syncs historical security prices (and details)
def sync_prices def import_security_prices
unless Security.provider unless Security.provider
Rails.logger.warn("No provider configured for MarketDataSyncer.sync_prices, skipping sync") Rails.logger.warn("No provider configured for MarketDataImporter.import_security_prices, skipping sync")
return return
end end
@ -33,9 +33,9 @@ class MarketDataSyncer
end end
end end
def sync_exchange_rates def import_exchange_rates
unless ExchangeRate.provider unless ExchangeRate.provider
Rails.logger.warn("No provider configured for MarketDataSyncer.sync_exchange_rates, skipping sync") Rails.logger.warn("No provider configured for MarketDataImporter.import_exchange_rates, skipping sync")
return return
end end
@ -124,7 +124,7 @@ class MarketDataSyncer
valid_modes = [ :full, :snapshot ] valid_modes = [ :full, :snapshot ]
unless valid_modes.include?(mode.to_sym) unless valid_modes.include?(mode.to_sym)
raise InvalidModeError, "Invalid mode for MarketDataSyncer, can only be :full or :snapshot, but was #{mode}" raise InvalidModeError, "Invalid mode for MarketDataImporter, can only be :full or :snapshot, but was #{mode}"
end end
mode.to_sym mode.to_sym

View file

@ -1,7 +1,7 @@
require "test_helper" require "test_helper"
require "ostruct" require "ostruct"
class Account::MarketDataSyncerTest < ActiveSupport::TestCase class Account::MarketDataImporterTest < ActiveSupport::TestCase
include ProviderTestHelper include ProviderTestHelper
PROVIDER_BUFFER = 5.days PROVIDER_BUFFER = 5.days
@ -49,7 +49,7 @@ class Account::MarketDataSyncerTest < ActiveSupport::TestCase
])) ]))
before = ExchangeRate.count before = ExchangeRate.count
Account::MarketDataSyncer.new(account).sync_market_data Account::MarketDataImporter.new(account).import_all
after = ExchangeRate.count after = ExchangeRate.count
assert_operator after, :>, before, "Should insert at least one new exchange-rate row" assert_operator after, :>, before, "Should insert at least one new exchange-rate row"
@ -100,7 +100,7 @@ class Account::MarketDataSyncerTest < ActiveSupport::TestCase
# Ignore exchange-rate calls for this test # Ignore exchange-rate calls for this test
@provider.stubs(:fetch_exchange_rates).returns(provider_success_response([])) @provider.stubs(:fetch_exchange_rates).returns(provider_success_response([]))
Account::MarketDataSyncer.new(account).sync_market_data Account::MarketDataImporter.new(account).import_all
assert_equal 1, Security::Price.where(security: security, date: trade_date).count assert_equal 1, Security::Price.where(security: security, date: trade_date).count
end end

View file

@ -1,10 +1,10 @@
require "test_helper" require "test_helper"
require "ostruct" require "ostruct"
class MarketDataSyncerTest < ActiveSupport::TestCase class MarketDataImporterTest < ActiveSupport::TestCase
include ProviderTestHelper include ProviderTestHelper
SNAPSHOT_START_DATE = MarketDataSyncer::SNAPSHOT_DAYS.days.ago.to_date SNAPSHOT_START_DATE = MarketDataImporter::SNAPSHOT_DAYS.days.ago.to_date
PROVIDER_BUFFER = 5.days PROVIDER_BUFFER = 5.days
setup do setup do
@ -47,7 +47,7 @@ class MarketDataSyncerTest < ActiveSupport::TestCase
])) ]))
before = ExchangeRate.count before = ExchangeRate.count
MarketDataSyncer.new(mode: :snapshot).sync_exchange_rates MarketDataImporter.new(mode: :snapshot).import_exchange_rates
after = ExchangeRate.count after = ExchangeRate.count
assert_operator after, :>, before, "Should insert at least one new exchange-rate row" assert_operator after, :>, before, "Should insert at least one new exchange-rate row"
@ -78,7 +78,7 @@ class MarketDataSyncerTest < ActiveSupport::TestCase
# Ignore exchange rate calls for this test # Ignore exchange rate calls for this test
@provider.stubs(:fetch_exchange_rates).returns(provider_success_response([])) @provider.stubs(:fetch_exchange_rates).returns(provider_success_response([]))
MarketDataSyncer.new(mode: :snapshot).sync_prices MarketDataImporter.new(mode: :snapshot).import_security_prices
assert_equal 1, Security::Price.where(security: security, date: SNAPSHOT_START_DATE).count assert_equal 1, Security::Price.where(security: security, date: SNAPSHOT_START_DATE).count
end end