mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-18 20:59:39 +02:00
Improve chart performance and gapfilling (#2306)
This commit is contained in:
parent
e1b81ef879
commit
6e202bd7ec
10 changed files with 382 additions and 196 deletions
|
@ -1,61 +1,46 @@
|
|||
require "test_helper"
|
||||
|
||||
class Account::ChartableTest < ActiveSupport::TestCase
|
||||
test "generates gapfilled balance series" do
|
||||
test "generates series and memoizes" do
|
||||
account = accounts(:depository)
|
||||
account.balances.delete_all
|
||||
|
||||
account.balances.create!(date: 20.days.ago.to_date, balance: 5000, currency: "USD")
|
||||
account.balances.create!(date: 10.days.ago.to_date, balance: 5000, currency: "USD")
|
||||
test_series = mock
|
||||
builder1 = mock
|
||||
builder2 = mock
|
||||
|
||||
period = Period.last_30_days
|
||||
series = account.balance_series(period: period)
|
||||
assert_equal period.days, series.values.count
|
||||
assert_equal 0, series.values.first.trend.current.amount
|
||||
assert_equal 5000, series.values.find { |v| v.date == 20.days.ago.to_date }.trend.current.amount
|
||||
assert_equal 5000, series.values.find { |v| v.date == 10.days.ago.to_date }.trend.current.amount
|
||||
assert_equal 5000, series.values.last.trend.current.amount
|
||||
end
|
||||
Balance::ChartSeriesBuilder.expects(:new)
|
||||
.with(
|
||||
account_ids: [ account.id ],
|
||||
currency: account.currency,
|
||||
period: Period.last_30_days,
|
||||
favorable_direction: account.favorable_direction,
|
||||
interval: nil
|
||||
)
|
||||
.returns(builder1)
|
||||
.once
|
||||
|
||||
test "combines assets and liabilities for multiple accounts properly" do
|
||||
family = families(:empty)
|
||||
Balance::ChartSeriesBuilder.expects(:new)
|
||||
.with(
|
||||
account_ids: [ account.id ],
|
||||
currency: account.currency,
|
||||
period: Period.last_90_days, # Period changed, so memoization should be invalidated
|
||||
favorable_direction: account.favorable_direction,
|
||||
interval: nil
|
||||
)
|
||||
.returns(builder2)
|
||||
.once
|
||||
|
||||
asset = family.accounts.create!(name: "Asset", currency: "USD", balance: 5000, accountable: Depository.new)
|
||||
liability = family.accounts.create!(name: "Liability", currency: "USD", balance: 2000, accountable: CreditCard.new)
|
||||
builder1.expects(:balance_series).returns(test_series).twice
|
||||
series1 = account.balance_series
|
||||
memoized_series1 = account.balance_series
|
||||
|
||||
asset.balances.create!(date: 20.days.ago.to_date, balance: 4000, currency: "USD")
|
||||
asset.balances.create!(date: 10.days.ago.to_date, balance: 5000, currency: "USD")
|
||||
builder2.expects(:balance_series).returns(test_series).twice
|
||||
builder2.expects(:cash_balance_series).returns(test_series).once
|
||||
builder2.expects(:holdings_balance_series).returns(test_series).once
|
||||
|
||||
liability.balances.create!(date: 20.days.ago.to_date, balance: 1000, currency: "USD")
|
||||
liability.balances.create!(date: 10.days.ago.to_date, balance: 1500, currency: "USD")
|
||||
|
||||
series = family.accounts.balance_series(currency: "USD", period: Period.last_30_days)
|
||||
|
||||
assert_equal 0, series.values.first.trend.current.amount
|
||||
assert_equal 3000, series.values.find { |v| v.date == 20.days.ago.to_date }.trend.current.amount
|
||||
assert_equal 3500, series.values.last.trend.current.amount
|
||||
end
|
||||
|
||||
test "generates correct totals for multi currency families" do
|
||||
family = families(:empty)
|
||||
family.update!(currency: "USD")
|
||||
|
||||
usd_account = family.accounts.create!(name: "Asset", currency: "USD", balance: 5000, accountable: Depository.new)
|
||||
eur_account = family.accounts.create!(name: "Asset", currency: "EUR", balance: 1000, accountable: Depository.new)
|
||||
|
||||
usd_account.balances.create!(date: 3.days.ago.to_date, balance: 5000, currency: "USD")
|
||||
eur_account.balances.create!(date: 3.days.ago.to_date, balance: 1000, currency: "EUR")
|
||||
|
||||
# 1 EUR = 1.1 USD, so 1000 EUR = 1100 USD
|
||||
ExchangeRate.create!(from_currency: "EUR", to_currency: "USD", date: 3.days.ago.to_date, rate: 1.1)
|
||||
ExchangeRate.create!(from_currency: "EUR", to_currency: "USD", date: 2.days.ago.to_date, rate: 1.1)
|
||||
ExchangeRate.create!(from_currency: "EUR", to_currency: "USD", date: 1.days.ago.to_date, rate: 1.1)
|
||||
ExchangeRate.create!(from_currency: "EUR", to_currency: "USD", date: Date.current, rate: 1.1)
|
||||
|
||||
series = family.accounts.balance_series(currency: "USD", period: Period.last_7_days)
|
||||
|
||||
assert_equal 0, series.values.first.trend.current.amount
|
||||
assert_equal 6100, series.values.find { |v| v.date == 3.days.ago.to_date }.trend.current.amount
|
||||
assert_equal 6100, series.values.last.trend.current.amount
|
||||
series2 = account.balance_series(period: Period.last_90_days)
|
||||
memoized_series2 = account.balance_series(period: Period.last_90_days)
|
||||
memoized_series2_cash_view = account.balance_series(period: Period.last_90_days, view: :cash_balance)
|
||||
memoized_series2_holdings_view = account.balance_series(period: Period.last_90_days, view: :holdings_balance)
|
||||
end
|
||||
end
|
||||
|
|
128
test/models/balance/chart_series_builder_test.rb
Normal file
128
test/models/balance/chart_series_builder_test.rb
Normal file
|
@ -0,0 +1,128 @@
|
|||
require "test_helper"
|
||||
|
||||
class Balance::ChartSeriesBuilderTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
end
|
||||
|
||||
test "balance series with fallbacks and gapfills" do
|
||||
account = accounts(:depository)
|
||||
account.balances.destroy_all
|
||||
|
||||
# With gaps
|
||||
account.balances.create!(date: 3.days.ago.to_date, balance: 1000, currency: "USD")
|
||||
account.balances.create!(date: 1.day.ago.to_date, balance: 1100, currency: "USD")
|
||||
account.balances.create!(date: Date.current, balance: 1200, currency: "USD")
|
||||
|
||||
builder = Balance::ChartSeriesBuilder.new(
|
||||
account_ids: [ account.id ],
|
||||
currency: "USD",
|
||||
period: Period.last_30_days,
|
||||
interval: "1 day"
|
||||
)
|
||||
|
||||
assert_equal 31, builder.balance_series.size # Last 30 days == 31 total balances
|
||||
assert_equal 0, builder.balance_series.first.value
|
||||
|
||||
expected = [
|
||||
0, # No value, so fallback to 0
|
||||
1000,
|
||||
1000, # Last observation carried forward
|
||||
1100,
|
||||
1200
|
||||
]
|
||||
|
||||
assert_equal expected, builder.balance_series.last(5).map { |v| v.value.amount }
|
||||
end
|
||||
|
||||
test "exchange rates apply locf when missing" do
|
||||
account = accounts(:depository)
|
||||
account.balances.destroy_all
|
||||
|
||||
account.balances.create!(date: 2.days.ago.to_date, balance: 1000, currency: "USD")
|
||||
account.balances.create!(date: 1.day.ago.to_date, balance: 1100, currency: "USD")
|
||||
account.balances.create!(date: Date.current, balance: 1200, currency: "USD")
|
||||
|
||||
builder = Balance::ChartSeriesBuilder.new(
|
||||
account_ids: [ account.id ],
|
||||
currency: "EUR", # Will need to convert existing balances to EUR
|
||||
period: Period.custom(start_date: 2.days.ago.to_date, end_date: Date.current),
|
||||
interval: "1 day"
|
||||
)
|
||||
|
||||
# Only 1 rate in DB. We'll be missing the first and last days in the series.
|
||||
# This rate should be applied to 1 day ago and today, but not 2 days ago (will fall back to 1)
|
||||
ExchangeRate.create!(date: 1.day.ago.to_date, from_currency: "USD", to_currency: "EUR", rate: 2)
|
||||
|
||||
expected = [
|
||||
1000, # No rate available, so fall back to 1:1 conversion (1000 USD = 1000 EUR)
|
||||
2200, # Rate available, so use 2:1 conversion (1100 USD = 2200 EUR)
|
||||
2400 # Rate NOT available, but LOCF will use the last available rate, so use 2:1 conversion (1200 USD = 2400 EUR)
|
||||
]
|
||||
|
||||
assert_equal expected, builder.balance_series.map { |v| v.value.amount }
|
||||
end
|
||||
|
||||
test "combines asset and liability accounts properly" do
|
||||
asset_account = accounts(:depository)
|
||||
liability_account = accounts(:credit_card)
|
||||
|
||||
Balance.destroy_all
|
||||
|
||||
asset_account.balances.create!(date: 3.days.ago.to_date, balance: 500, currency: "USD")
|
||||
asset_account.balances.create!(date: 1.day.ago.to_date, balance: 1000, currency: "USD")
|
||||
asset_account.balances.create!(date: Date.current, balance: 1000, currency: "USD")
|
||||
|
||||
liability_account.balances.create!(date: 3.days.ago.to_date, balance: 200, currency: "USD")
|
||||
liability_account.balances.create!(date: 2.days.ago.to_date, balance: 200, currency: "USD")
|
||||
liability_account.balances.create!(date: Date.current, balance: 100, currency: "USD")
|
||||
|
||||
builder = Balance::ChartSeriesBuilder.new(
|
||||
account_ids: [ asset_account.id, liability_account.id ],
|
||||
currency: "USD",
|
||||
period: Period.custom(start_date: 4.days.ago.to_date, end_date: Date.current),
|
||||
interval: "1 day"
|
||||
)
|
||||
|
||||
expected = [
|
||||
0, # No asset or liability balances - 4 days ago
|
||||
300, # 500 - 200 = 300 - 3 days ago
|
||||
300, # 500 - 200 = 300 (500 is locf) - 2 days ago
|
||||
800, # 1000 - 200 = 800 (200 is locf) - 1 day ago
|
||||
900 # 1000 - 100 = 900 - today
|
||||
]
|
||||
|
||||
assert_equal expected, builder.balance_series.map { |v| v.value.amount }
|
||||
end
|
||||
|
||||
test "when favorable direction is down balance signage inverts" do
|
||||
account = accounts(:credit_card)
|
||||
account.balances.destroy_all
|
||||
|
||||
account.balances.create!(date: 1.day.ago.to_date, balance: 1000, currency: "USD")
|
||||
account.balances.create!(date: Date.current, balance: 500, currency: "USD")
|
||||
|
||||
builder = Balance::ChartSeriesBuilder.new(
|
||||
account_ids: [ account.id ],
|
||||
currency: "USD",
|
||||
period: Period.custom(start_date: 1.day.ago.to_date, end_date: Date.current),
|
||||
favorable_direction: "up"
|
||||
)
|
||||
|
||||
# Since favorable direction is up and balances are liabilities, the values should be negative
|
||||
expected = [ -1000, -500 ]
|
||||
|
||||
assert_equal expected, builder.balance_series.map { |v| v.value.amount }
|
||||
|
||||
builder = Balance::ChartSeriesBuilder.new(
|
||||
account_ids: [ account.id ],
|
||||
currency: "USD",
|
||||
period: Period.custom(start_date: 1.day.ago.to_date, end_date: Date.current),
|
||||
favorable_direction: "down"
|
||||
)
|
||||
|
||||
# Since favorable direction is down and balances are liabilities, the values should be positive
|
||||
expected = [ 1000, 500 ]
|
||||
|
||||
assert_equal expected, builder.balance_series.map { |v| v.value.amount }
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue