mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-07 22:45:20 +02:00
Fix investment valuation calculations and recon display
This commit is contained in:
parent
91f1dceead
commit
0619102273
4 changed files with 92 additions and 33 deletions
|
@ -63,9 +63,14 @@ class UI::Account::BalanceReconciliation < ApplicationComponent
|
|||
{ label: "Start balance", value: balance.start_balance_money, tooltip: "The total portfolio value at the beginning of this day", style: :start }
|
||||
]
|
||||
|
||||
items << { label: "Cash flows", value: net_cash_flow, tooltip: "Deposits and withdrawals during the day", style: :flow } if net_cash_flow != 0
|
||||
items << { label: "Trading activity", value: net_non_cash_flow, tooltip: "Net change from buying and selling securities", style: :flow } if net_non_cash_flow != 0
|
||||
items << { label: "Market changes", value: balance.net_market_flows_money, tooltip: "Value changes from market price movements", style: :flow } if balance.net_market_flows != 0
|
||||
# Change in brokerage cash (includes deposits, withdrawals, and cash from trades)
|
||||
items << { label: "Change in brokerage cash", value: net_cash_flow, tooltip: "Net change in cash from deposits, withdrawals, and trades", style: :flow }
|
||||
|
||||
# Change in holdings from trading activity
|
||||
items << { label: "Change in holdings (buys/sells)", value: net_non_cash_flow, tooltip: "Impact on holdings from buying and selling securities", style: :flow }
|
||||
|
||||
# Market price changes
|
||||
items << { label: "Change in holdings (market price activity)", value: balance.net_market_flows_money, tooltip: "Change in holdings value from market price movements", style: :flow }
|
||||
|
||||
if has_adjustments?
|
||||
items << { label: "End balance", value: end_balance_before_adjustments, tooltip: "The calculated balance after all activity", style: :subtotal }
|
||||
|
|
|
@ -29,16 +29,16 @@ class Balance::BaseCalculator
|
|||
end
|
||||
end
|
||||
|
||||
def cash_adjustments_for_date(start_cash, net_cash_flows, valuation)
|
||||
return 0 unless valuation && account.balance_type != :non_cash
|
||||
def cash_adjustments_for_date(start_cash, end_cash, net_cash_flows)
|
||||
return 0 unless account.balance_type != :non_cash
|
||||
|
||||
valuation.amount - start_cash - net_cash_flows
|
||||
end_cash - start_cash - net_cash_flows
|
||||
end
|
||||
|
||||
def non_cash_adjustments_for_date(start_non_cash, non_cash_flows, valuation)
|
||||
return 0 unless valuation && account.balance_type == :non_cash
|
||||
def non_cash_adjustments_for_date(start_non_cash, end_non_cash, non_cash_flows)
|
||||
return 0 unless account.balance_type == :non_cash
|
||||
|
||||
valuation.amount - start_non_cash - non_cash_flows
|
||||
end_non_cash - start_non_cash - non_cash_flows
|
||||
end
|
||||
|
||||
# If holdings value goes from $100 -> $200 (change_holdings_value is $100)
|
||||
|
|
|
@ -2,10 +2,10 @@ class Balance::ForwardCalculator < Balance::BaseCalculator
|
|||
def calculate
|
||||
Rails.logger.tagged("Balance::ForwardCalculator") do
|
||||
start_cash_balance = derive_cash_balance_on_date_from_total(
|
||||
total_balance: 0,
|
||||
total_balance: account.opening_anchor_balance,
|
||||
date: account.opening_anchor_date
|
||||
)
|
||||
start_non_cash_balance = 0
|
||||
start_non_cash_balance = account.opening_anchor_balance - start_cash_balance
|
||||
|
||||
calc_start_date.upto(calc_end_date).map do |date|
|
||||
valuation = sync_cache.get_valuation(date)
|
||||
|
@ -24,6 +24,9 @@ class Balance::ForwardCalculator < Balance::BaseCalculator
|
|||
flows = flows_for_date(date)
|
||||
market_value_change = market_value_change_on_date(date, flows)
|
||||
|
||||
cash_adjustments = cash_adjustments_for_date(start_cash_balance, end_cash_balance, (flows[:cash_inflows] - flows[:cash_outflows]) * flows_factor)
|
||||
non_cash_adjustments = non_cash_adjustments_for_date(start_non_cash_balance, end_non_cash_balance, (flows[:non_cash_inflows] - flows[:non_cash_outflows]) * flows_factor)
|
||||
|
||||
output_balance = build_balance(
|
||||
date: date,
|
||||
balance: end_cash_balance + end_non_cash_balance,
|
||||
|
@ -34,8 +37,8 @@ class Balance::ForwardCalculator < Balance::BaseCalculator
|
|||
cash_outflows: flows[:cash_outflows],
|
||||
non_cash_inflows: flows[:non_cash_inflows],
|
||||
non_cash_outflows: flows[:non_cash_outflows],
|
||||
cash_adjustments: cash_adjustments_for_date(start_cash_balance, flows[:cash_inflows] - flows[:cash_outflows], valuation),
|
||||
non_cash_adjustments: non_cash_adjustments_for_date(start_non_cash_balance, flows[:non_cash_inflows] - flows[:non_cash_outflows], valuation),
|
||||
cash_adjustments: cash_adjustments,
|
||||
non_cash_adjustments: non_cash_adjustments,
|
||||
net_market_flows: market_value_change
|
||||
)
|
||||
|
||||
|
@ -75,4 +78,8 @@ class Balance::ForwardCalculator < Balance::BaseCalculator
|
|||
def derive_end_non_cash_balance(start_non_cash_balance:, date:)
|
||||
derive_non_cash_balance(start_non_cash_balance, date, direction: :forward)
|
||||
end
|
||||
|
||||
def flows_factor
|
||||
account.asset? ? 1 : -1
|
||||
end
|
||||
end
|
||||
|
|
|
@ -117,9 +117,9 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
{
|
||||
date: 3.days.ago.to_date,
|
||||
legacy_balances: { balance: 17000, cash_balance: 17000 },
|
||||
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 17000, end_non_cash: 0, end: 17000 },
|
||||
balances: { start: 17000, start_cash: 17000, start_non_cash: 0, end_cash: 17000, end_non_cash: 0, end: 17000 },
|
||||
flows: 0,
|
||||
adjustments: { cash_adjustments: 17000, non_cash_adjustments: 0 }
|
||||
adjustments: 0
|
||||
},
|
||||
{
|
||||
date: 2.days.ago.to_date,
|
||||
|
@ -151,9 +151,9 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
{
|
||||
date: 3.days.ago.to_date,
|
||||
legacy_balances: { balance: 17000, cash_balance: 0.0 },
|
||||
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 0, end_non_cash: 17000, end: 17000 },
|
||||
balances: { start: 17000, start_cash: 0, start_non_cash: 17000, end_cash: 0, end_non_cash: 17000, end: 17000 },
|
||||
flows: 0,
|
||||
adjustments: { cash_adjustments: 0, non_cash_adjustments: 17000 }
|
||||
adjustments: 0
|
||||
},
|
||||
{
|
||||
date: 2.days.ago.to_date,
|
||||
|
@ -185,9 +185,9 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
{
|
||||
date: 3.days.ago.to_date,
|
||||
legacy_balances: { balance: 17000, cash_balance: 17000 },
|
||||
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 17000, end_non_cash: 0, end: 17000 },
|
||||
balances: { start: 17000, start_cash: 17000, start_non_cash: 0, end_cash: 17000, end_non_cash: 0, end: 17000 },
|
||||
flows: { market_flows: 0 },
|
||||
adjustments: { cash_adjustments: 17000, non_cash_adjustments: 0 } # Since no holdings present, adjustment is all cash
|
||||
adjustments: 0
|
||||
},
|
||||
{
|
||||
date: 2.days.ago.to_date,
|
||||
|
@ -222,9 +222,9 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
{
|
||||
date: 5.days.ago.to_date,
|
||||
legacy_balances: { balance: 20000, cash_balance: 20000 },
|
||||
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 },
|
||||
balances: { start: 20000, start_cash: 20000, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 },
|
||||
flows: 0,
|
||||
adjustments: { cash_adjustments: 20000, non_cash_adjustments: 0 }
|
||||
adjustments: 0
|
||||
},
|
||||
{
|
||||
date: 4.days.ago.to_date,
|
||||
|
@ -270,9 +270,9 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
{
|
||||
date: 5.days.ago.to_date,
|
||||
legacy_balances: { balance: 1000, cash_balance: 1000 },
|
||||
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 1000, end_non_cash: 0, end: 1000 },
|
||||
balances: { start: 1000, start_cash: 1000, start_non_cash: 0, end_cash: 1000, end_non_cash: 0, end: 1000 },
|
||||
flows: 0,
|
||||
adjustments: { cash_adjustments: 1000, non_cash_adjustments: 0 }
|
||||
adjustments: 0
|
||||
},
|
||||
{
|
||||
date: 4.days.ago.to_date,
|
||||
|
@ -318,9 +318,9 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
{
|
||||
date: 4.days.ago.to_date,
|
||||
legacy_balances: { balance: 20000, cash_balance: 20000 },
|
||||
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 },
|
||||
balances: { start: 20000, start_cash: 20000, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 },
|
||||
flows: 0,
|
||||
adjustments: { cash_adjustments: 20000, non_cash_adjustments: 0 }
|
||||
adjustments: 0
|
||||
},
|
||||
{
|
||||
date: 3.days.ago.to_date,
|
||||
|
@ -370,9 +370,9 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
{
|
||||
date: 4.days.ago.to_date,
|
||||
legacy_balances: { balance: 100, cash_balance: 100 },
|
||||
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 100, end_non_cash: 0, end: 100 },
|
||||
balances: { start: 100, start_cash: 100, start_non_cash: 0, end_cash: 100, end_non_cash: 0, end: 100 },
|
||||
flows: 0,
|
||||
adjustments: { cash_adjustments: 100, non_cash_adjustments: 0 }
|
||||
adjustments: 0
|
||||
},
|
||||
{
|
||||
date: 3.days.ago.to_date,
|
||||
|
@ -420,9 +420,9 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
{
|
||||
date: 2.days.ago.to_date,
|
||||
legacy_balances: { balance: 20000, cash_balance: 0 },
|
||||
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 0, end_non_cash: 20000, end: 20000 },
|
||||
balances: { start: 20000, start_cash: 0, start_non_cash: 20000, end_cash: 0, end_non_cash: 20000, end: 20000 },
|
||||
flows: 0,
|
||||
adjustments: { cash_adjustments: 0, non_cash_adjustments: 20000 } # Valuations adjust non-cash balance for non-cash accounts like Loans
|
||||
adjustments: 0
|
||||
},
|
||||
{
|
||||
date: 1.day.ago.to_date,
|
||||
|
@ -455,9 +455,9 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
{
|
||||
date: 3.days.ago.to_date,
|
||||
legacy_balances: { balance: 500000, cash_balance: 0 },
|
||||
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 0, end_non_cash: 500000, end: 500000 },
|
||||
balances: { start: 500000, start_cash: 0, start_non_cash: 500000, end_cash: 0, end_non_cash: 500000, end: 500000 },
|
||||
flows: 0,
|
||||
adjustments: { cash_adjustments: 0, non_cash_adjustments: 500000 }
|
||||
adjustments: 0
|
||||
},
|
||||
{
|
||||
date: 2.days.ago.to_date,
|
||||
|
@ -505,9 +505,9 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
{
|
||||
date: 3.days.ago.to_date,
|
||||
legacy_balances: { balance: 5000, cash_balance: 5000 },
|
||||
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 5000, end_non_cash: 0, end: 5000 },
|
||||
balances: { start: 5000, start_cash: 5000, start_non_cash: 0, end_cash: 5000, end_non_cash: 0, end: 5000 },
|
||||
flows: 0,
|
||||
adjustments: { cash_adjustments: 5000, non_cash_adjustments: 0 }
|
||||
adjustments: 0
|
||||
},
|
||||
{
|
||||
date: 2.days.ago.to_date,
|
||||
|
@ -534,6 +534,53 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
)
|
||||
end
|
||||
|
||||
test "investment account can have valuations that override balance" do
|
||||
account = create_account_with_ledger(
|
||||
account: { type: Investment, currency: "USD" },
|
||||
entries: [
|
||||
{ type: "opening_anchor", date: 2.days.ago.to_date, balance: 5000 },
|
||||
{ type: "reconciliation", date: 1.day.ago.to_date, balance: 10000 }
|
||||
],
|
||||
holdings: [
|
||||
{ date: 3.days.ago.to_date, ticker: "AAPL", qty: 10, price: 100, amount: 1000 },
|
||||
{ date: 2.days.ago.to_date, ticker: "AAPL", qty: 10, price: 100, amount: 1000 },
|
||||
{ date: 1.day.ago.to_date, ticker: "AAPL", qty: 10, price: 110, amount: 1100 },
|
||||
{ date: Date.current, ticker: "AAPL", qty: 10, price: 120, amount: 1200 }
|
||||
]
|
||||
)
|
||||
|
||||
# Given constant prices, overall balance (account value) should be constant
|
||||
# (the single trade doesn't affect balance; it just alters cash vs. holdings composition)
|
||||
calculated = Balance::ForwardCalculator.new(account).calculate
|
||||
|
||||
assert_calculated_ledger_balances(
|
||||
calculated_data: calculated,
|
||||
expected_data: [
|
||||
{
|
||||
date: 2.days.ago.to_date,
|
||||
legacy_balances: { balance: 5000, cash_balance: 4000 },
|
||||
balances: { start: 5000, start_cash: 4000, start_non_cash: 1000, end_cash: 4000, end_non_cash: 1000, end: 5000 },
|
||||
flows: 0,
|
||||
adjustments: 0
|
||||
},
|
||||
{
|
||||
date: 1.day.ago.to_date,
|
||||
legacy_balances: { balance: 10000, cash_balance: 8900 },
|
||||
balances: { start: 5000, start_cash: 4000, start_non_cash: 1000, end_cash: 8900, end_non_cash: 1100, end: 10000 },
|
||||
flows: { net_market_flows: 100 },
|
||||
adjustments: { cash_adjustments: 4900, non_cash_adjustments: 0 }
|
||||
},
|
||||
{
|
||||
date: Date.current,
|
||||
legacy_balances: { balance: 10100, cash_balance: 8900 },
|
||||
balances: { start: 10000, start_cash: 8900, start_non_cash: 1100, end_cash: 8900, end_non_cash: 1200, end: 10100 },
|
||||
flows: { net_market_flows: 100 },
|
||||
adjustments: 0
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
def assert_balances(calculated_data:, expected_balances:)
|
||||
# Sort calculated data by date to ensure consistent ordering
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue