Files
sure/app/models/investment_statement/totals.rb
soky srm 6ebe8da928 Add investment tracking to expenses (#381)
* Add investment tracking to expenses

Add new sections to dashboard and reporting around investments.

* Create investment-integration-assessment.md

* Delete .claude/settings.local.json

Signed-off-by: soky srm <sokysrm@gmail.com>

* Category trades

* Simplify

* Simplification and test fixes

* FIX merge

* Update views

* Update 20251125141213_add_category_to_trades.rb

* FIX tests

* FIX statements and account status

* cleanup

* Add default cat for csv imports

* Delete docs/roadmap/investment-integration-assessment.md

Signed-off-by: soky srm <sokysrm@gmail.com>

* Update trend calculation

Use already existing column cost basis for trend calculation
   - Current value: qty * price (already stored as amount)
  - Cost basis total: qty * cost_basis
  - Unrealized gain: current value - cost basis total
Fixes N+1 query also

---------

Signed-off-by: soky srm <sokysrm@gmail.com>
2026-01-09 13:03:40 +01:00

57 lines
1.8 KiB
Ruby

class InvestmentStatement::Totals
def initialize(family, trades_scope:)
@family = family
@trades_scope = trades_scope
end
def call
result = ActiveRecord::Base.connection.select_one(query_sql)
{
contributions: result["contributions"]&.to_d || 0,
withdrawals: result["withdrawals"]&.to_d || 0,
dividends: 0, # Dividends come through as transactions, not trades
interest: 0, # Interest comes through as transactions, not trades
trades_count: result["trades_count"]&.to_i || 0
}
end
private
def query_sql
ActiveRecord::Base.sanitize_sql_array([
aggregation_sql,
sql_params
])
end
# Aggregate trades by direction (buy vs sell)
# Buys (qty > 0) = contributions (cash going out to buy securities)
# Sells (qty < 0) = withdrawals (cash coming in from selling securities)
def aggregation_sql
<<~SQL
SELECT
COALESCE(SUM(CASE WHEN t.qty > 0 THEN ABS(ae.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as contributions,
COALESCE(SUM(CASE WHEN t.qty < 0 THEN ABS(ae.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as withdrawals,
COUNT(t.id) as trades_count
FROM (#{@trades_scope.to_sql}) t
JOIN entries ae ON ae.entryable_id = t.id AND ae.entryable_type = 'Trade'
JOIN accounts a ON a.id = ae.account_id
LEFT JOIN exchange_rates er ON (
er.date = ae.date AND
er.from_currency = ae.currency AND
er.to_currency = :target_currency
)
WHERE a.family_id = :family_id
AND a.status IN ('draft', 'active')
AND ae.excluded = false
SQL
end
def sql_params
{
family_id: @family.id,
target_currency: @family.currency
}
end
end