diff --git a/app/jobs/sync_market_data_job.rb b/app/jobs/sync_market_data_job.rb index db34a41a..9580a467 100644 --- a/app/jobs/sync_market_data_job.rb +++ b/app/jobs/sync_market_data_job.rb @@ -15,6 +15,6 @@ class SyncMarketDataJob < ApplicationJob mode = opts.fetch(:mode, :full) 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 diff --git a/app/models/account/market_data_importer.rb b/app/models/account/market_data_importer.rb new file mode 100644 index 00000000..b344c1cb --- /dev/null +++ b/app/models/account/market_data_importer.rb @@ -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 diff --git a/app/models/account/market_data_syncer.rb b/app/models/account/market_data_syncer.rb deleted file mode 100644 index b223d229..00000000 --- a/app/models/account/market_data_syncer.rb +++ /dev/null @@ -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 diff --git a/app/models/account/syncer.rb b/app/models/account/syncer.rb index de63f5e8..f5c12175 100644 --- a/app/models/account/syncer.rb +++ b/app/models/account/syncer.rb @@ -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 have reasonable fallbacks for missing market data. def sync_market_data - Account::MarketDataSyncer.new(account).sync_market_data + Account::MarketDataImporter.new(account).import_all rescue => e Rails.logger.error("Error syncing market data for account #{account.id}: #{e.message}") Sentry.capture_exception(e) diff --git a/app/models/market_data_syncer.rb b/app/models/market_data_importer.rb similarity index 88% rename from app/models/market_data_syncer.rb rename to app/models/market_data_importer.rb index 5567ba31..a16eb515 100644 --- a/app/models/market_data_syncer.rb +++ b/app/models/market_data_importer.rb @@ -1,4 +1,4 @@ -class MarketDataSyncer +class MarketDataImporter # By default, our graphs show 1M as the view, so by fetching 31 days, # we ensure we can always show an accurate default graph SNAPSHOT_DAYS = 31 @@ -10,15 +10,15 @@ class MarketDataSyncer @clear_cache = clear_cache end - def sync - sync_prices - sync_exchange_rates + def import_all + import_security_prices + import_exchange_rates end # Syncs historical security prices (and details) - def sync_prices + def import_security_prices 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 end @@ -33,9 +33,9 @@ class MarketDataSyncer end end - def sync_exchange_rates + def import_exchange_rates 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 end @@ -124,7 +124,7 @@ class MarketDataSyncer valid_modes = [ :full, :snapshot ] 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 mode.to_sym diff --git a/test/models/account/market_data_syncer_test.rb b/test/models/account/market_data_import_test.rb similarity index 94% rename from test/models/account/market_data_syncer_test.rb rename to test/models/account/market_data_import_test.rb index 596798f5..c4eea68e 100644 --- a/test/models/account/market_data_syncer_test.rb +++ b/test/models/account/market_data_import_test.rb @@ -1,7 +1,7 @@ require "test_helper" require "ostruct" -class Account::MarketDataSyncerTest < ActiveSupport::TestCase +class Account::MarketDataImporterTest < ActiveSupport::TestCase include ProviderTestHelper PROVIDER_BUFFER = 5.days @@ -49,7 +49,7 @@ class Account::MarketDataSyncerTest < ActiveSupport::TestCase ])) before = ExchangeRate.count - Account::MarketDataSyncer.new(account).sync_market_data + Account::MarketDataImporter.new(account).import_all after = ExchangeRate.count 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 @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 end diff --git a/test/models/market_data_syncer_test.rb b/test/models/market_data_importer_test.rb similarity index 91% rename from test/models/market_data_syncer_test.rb rename to test/models/market_data_importer_test.rb index 8a9db1f5..b39bf0ad 100644 --- a/test/models/market_data_syncer_test.rb +++ b/test/models/market_data_importer_test.rb @@ -1,10 +1,10 @@ require "test_helper" require "ostruct" -class MarketDataSyncerTest < ActiveSupport::TestCase +class MarketDataImporterTest < ActiveSupport::TestCase 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 setup do @@ -47,7 +47,7 @@ class MarketDataSyncerTest < ActiveSupport::TestCase ])) before = ExchangeRate.count - MarketDataSyncer.new(mode: :snapshot).sync_exchange_rates + MarketDataImporter.new(mode: :snapshot).import_exchange_rates after = ExchangeRate.count 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 @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 end