mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-10 07:55:21 +02:00
Calculate cash and holdings series
This commit is contained in:
parent
a79bc424da
commit
d7fbf14af7
2 changed files with 93 additions and 47 deletions
|
@ -2,46 +2,81 @@ module Account::Chartable
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def cash_balance_series(currency:, period: Period.last_30_days, favorable_direction: "up")
|
||||
generate_series(
|
||||
balances: fetch_balances(currency: currency, period: period),
|
||||
period: period,
|
||||
currency: currency,
|
||||
favorable_direction: favorable_direction,
|
||||
balance_type: :cash_balance
|
||||
)
|
||||
end
|
||||
|
||||
def holdings_series(currency:, period: Period.last_30_days, favorable_direction: "up")
|
||||
generate_series(
|
||||
balances: fetch_balances(currency: currency, period: period),
|
||||
period: period,
|
||||
currency: currency,
|
||||
favorable_direction: favorable_direction,
|
||||
balance_type: :holdings_balance
|
||||
)
|
||||
end
|
||||
|
||||
def balance_series(currency:, period: Period.last_30_days, favorable_direction: "up")
|
||||
balances = Account::Balance.find_by_sql([
|
||||
balance_series_query,
|
||||
{
|
||||
start_date: period.start_date,
|
||||
end_date: period.end_date,
|
||||
interval: period.interval,
|
||||
target_currency: currency
|
||||
}
|
||||
])
|
||||
|
||||
balances = gapfill_balances(balances)
|
||||
balances = invert_balances(balances) if favorable_direction == "down"
|
||||
|
||||
values = [ nil, *balances ].each_cons(2).map do |prev, curr|
|
||||
Series::Value.new(
|
||||
date: curr.date,
|
||||
date_formatted: I18n.l(curr.date, format: :long),
|
||||
trend: Trend.new(
|
||||
current: Money.new(curr.balance, currency),
|
||||
previous: prev.nil? ? nil : Money.new(prev.balance, currency),
|
||||
favorable_direction: favorable_direction
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
Series.new(
|
||||
start_date: period.start_date,
|
||||
end_date: period.end_date,
|
||||
interval: period.interval,
|
||||
trend: Trend.new(
|
||||
current: Money.new(balances.last&.balance || 0, currency),
|
||||
previous: Money.new(balances.first&.balance || 0, currency),
|
||||
favorable_direction: favorable_direction
|
||||
),
|
||||
values: values
|
||||
generate_series(
|
||||
balances: fetch_balances(currency: currency, period: period),
|
||||
period: period,
|
||||
currency: currency,
|
||||
favorable_direction: favorable_direction,
|
||||
balance_type: :balance
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
def fetch_balances(currency:, period:)
|
||||
@_memoized_balances ||= {}
|
||||
cache_key = "#{currency}_#{period.start_date}_#{period.end_date}_#{period.interval}"
|
||||
|
||||
@_memoized_balances[cache_key] ||= Account::Balance.find_by_sql([
|
||||
balance_series_query,
|
||||
{
|
||||
start_date: period.start_date,
|
||||
end_date: period.end_date,
|
||||
interval: period.interval,
|
||||
target_currency: currency
|
||||
}
|
||||
])
|
||||
end
|
||||
|
||||
def generate_series(balances:, period:, currency:, favorable_direction:, balance_type:)
|
||||
balances = gapfill_balances(balances, balance_type)
|
||||
balances = invert_balances(balances, balance_type) if favorable_direction == "down"
|
||||
|
||||
values = [ nil, *balances ].each_cons(2).map do |prev, curr|
|
||||
Series::Value.new(
|
||||
date: curr.date,
|
||||
date_formatted: I18n.l(curr.date, format: :long),
|
||||
trend: Trend.new(
|
||||
current: Money.new(curr.send(balance_type), currency),
|
||||
previous: prev.nil? ? nil : Money.new(prev.send(balance_type), currency),
|
||||
favorable_direction: favorable_direction
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
Series.new(
|
||||
start_date: period.start_date,
|
||||
end_date: period.end_date,
|
||||
interval: period.interval,
|
||||
trend: Trend.new(
|
||||
current: Money.new(balances.last&.send(balance_type) || 0, currency),
|
||||
previous: Money.new(balances.first&.send(balance_type) || 0, currency),
|
||||
favorable_direction: favorable_direction
|
||||
),
|
||||
values: values
|
||||
)
|
||||
end
|
||||
|
||||
def balance_series_query
|
||||
<<~SQL
|
||||
WITH dates as (
|
||||
|
@ -52,6 +87,8 @@ module Account::Chartable
|
|||
SELECT
|
||||
d.date,
|
||||
SUM(CASE WHEN accounts.classification = 'asset' THEN ab.balance ELSE -ab.balance END * COALESCE(er.rate, 1)) as balance,
|
||||
SUM(CASE WHEN accounts.classification = 'asset' THEN ab.cash_balance ELSE -ab.cash_balance END * COALESCE(er.rate, 1)) as cash_balance,
|
||||
SUM(CASE WHEN accounts.classification = 'asset' THEN ab.balance - ab.cash_balance ELSE 0 END * COALESCE(er.rate, 1)) as holdings_balance,
|
||||
COUNT(CASE WHEN accounts.currency <> :target_currency AND er.rate IS NULL THEN 1 END) as missing_rates
|
||||
FROM dates d
|
||||
LEFT JOIN accounts ON accounts.id IN (#{all.select(:id).to_sql})
|
||||
|
@ -70,23 +107,21 @@ module Account::Chartable
|
|||
SQL
|
||||
end
|
||||
|
||||
def invert_balances(balances)
|
||||
def invert_balances(balances, balance_type)
|
||||
balances.map do |balance|
|
||||
balance.balance = -balance.balance
|
||||
balance.send("#{balance_type}=", -balance.send(balance_type))
|
||||
balance
|
||||
end
|
||||
end
|
||||
|
||||
def gapfill_balances(balances)
|
||||
def gapfill_balances(balances, balance_type)
|
||||
gapfilled = []
|
||||
|
||||
prev_balance = nil
|
||||
|
||||
[ nil, *balances ].each_cons(2).each_with_index do |(prev, curr), index|
|
||||
if index == 0 && curr.balance.nil?
|
||||
curr.balance = 0 # Ensure all series start with a non-nil balance
|
||||
elsif curr.balance.nil?
|
||||
curr.balance = prev.balance
|
||||
if index == 0 && curr.send(balance_type).nil?
|
||||
curr.send("#{balance_type}=", 0) # Ensure all series start with a non-nil balance
|
||||
elsif curr.send(balance_type).nil?
|
||||
curr.send("#{balance_type}=", prev.send(balance_type))
|
||||
end
|
||||
|
||||
gapfilled << curr
|
||||
|
|
|
@ -14,9 +14,20 @@
|
|||
<%= tag.p format_money(account.balance_money), class: "text-primary text-3xl font-medium" %>
|
||||
</div>
|
||||
|
||||
<%= form_with url: request.path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
|
||||
<%= period_select form: form, selected: period %>
|
||||
<% end %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= form_with url: request.path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
|
||||
<%= form.select :chart_view,
|
||||
[["Total value", "balance"], ["Holdings", "holdings_balance"], ["Cash", "cash_balance"]],
|
||||
{ selected: params[:chart_view] || "balance" },
|
||||
class: "border border-secondary rounded-lg text-sm pr-7 cursor-pointer text-primary focus:outline-hidden focus:ring-0",
|
||||
data: { "auto-submit-form-target": "auto" }
|
||||
%>
|
||||
<% end %>
|
||||
|
||||
<%= form_with url: request.path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
|
||||
<%= period_select form: form, selected: period %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= turbo_frame_tag dom_id(account, :chart_details), src: chart_account_path(account, period: period.key) do %>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue