mirror of
https://github.com/we-promise/sure.git
synced 2026-04-08 06:44:52 +00:00
* 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>
104 lines
3.2 KiB
Ruby
104 lines
3.2 KiB
Ruby
class Holding::ForwardCalculator
|
|
attr_reader :account
|
|
|
|
def initialize(account)
|
|
@account = account
|
|
# Track cost basis per security: { security_id => { total_cost: BigDecimal, total_qty: BigDecimal } }
|
|
@cost_basis_tracker = Hash.new { |h, k| h[k] = { total_cost: BigDecimal("0"), total_qty: BigDecimal("0") } }
|
|
end
|
|
|
|
def calculate
|
|
Rails.logger.tagged("Holding::ForwardCalculator") do
|
|
current_portfolio = generate_starting_portfolio
|
|
next_portfolio = {}
|
|
holdings = []
|
|
|
|
account.start_date.upto(Date.current).each do |date|
|
|
trades = portfolio_cache.get_trades(date: date)
|
|
update_cost_basis_tracker(trades)
|
|
next_portfolio = transform_portfolio(current_portfolio, trades, direction: :forward)
|
|
holdings += build_holdings(next_portfolio, date)
|
|
current_portfolio = next_portfolio
|
|
end
|
|
|
|
Holding.gapfill(holdings)
|
|
end
|
|
end
|
|
|
|
private
|
|
def portfolio_cache
|
|
@portfolio_cache ||= Holding::PortfolioCache.new(account)
|
|
end
|
|
|
|
def empty_portfolio
|
|
securities = portfolio_cache.get_securities
|
|
securities.each_with_object({}) { |security, hash| hash[security.id] = 0 }
|
|
end
|
|
|
|
def generate_starting_portfolio
|
|
empty_portfolio
|
|
end
|
|
|
|
def transform_portfolio(previous_portfolio, trade_entries, direction: :forward)
|
|
new_quantities = previous_portfolio.dup
|
|
|
|
trade_entries.each do |trade_entry|
|
|
trade = trade_entry.entryable
|
|
security_id = trade.security_id
|
|
qty_change = trade.qty
|
|
qty_change = qty_change * -1 if direction == :reverse
|
|
new_quantities[security_id] = (new_quantities[security_id] || 0) + qty_change
|
|
end
|
|
|
|
new_quantities
|
|
end
|
|
|
|
def build_holdings(portfolio, date, price_source: nil)
|
|
portfolio.map do |security_id, qty|
|
|
price = portfolio_cache.get_price(security_id, date, source: price_source)
|
|
|
|
if price.nil?
|
|
next
|
|
end
|
|
|
|
Holding.new(
|
|
account_id: account.id,
|
|
security_id: security_id,
|
|
date: date,
|
|
qty: qty,
|
|
price: price.price,
|
|
currency: price.currency,
|
|
amount: qty * price.price,
|
|
cost_basis: cost_basis_for(security_id, price.currency)
|
|
)
|
|
end.compact
|
|
end
|
|
|
|
# Updates cost basis tracker with buy trades (qty > 0)
|
|
# Uses weighted average cost method
|
|
def update_cost_basis_tracker(trade_entries)
|
|
trade_entries.each do |trade_entry|
|
|
trade = trade_entry.entryable
|
|
next unless trade.qty > 0 # Only track buys
|
|
|
|
security_id = trade.security_id
|
|
tracker = @cost_basis_tracker[security_id]
|
|
|
|
# Convert trade price to account currency if needed
|
|
trade_price = Money.new(trade.price, trade.currency)
|
|
converted_price = trade_price.exchange_to(account.currency, fallback_rate: 1).amount
|
|
|
|
tracker[:total_cost] += converted_price * trade.qty
|
|
tracker[:total_qty] += trade.qty
|
|
end
|
|
end
|
|
|
|
# Returns the current cost basis for a security, or nil if no buys recorded
|
|
def cost_basis_for(security_id, currency)
|
|
tracker = @cost_basis_tracker[security_id]
|
|
return nil if tracker[:total_qty].zero?
|
|
|
|
tracker[:total_cost] / tracker[:total_qty]
|
|
end
|
|
end
|