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:
parent
b8903d0980
commit
e75dbc978b
7 changed files with 100 additions and 100 deletions
|
@ -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
|
||||||
|
|
82
app/models/account/market_data_importer.rb
Normal file
82
app/models/account/market_data_importer.rb
Normal 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
|
|
@ -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
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue