Fix avg_cost to return per-share cost basis (#1692)

* Fix avg_cost to return per-share cost basis

* Revert "Fix avg_cost to return per-share cost basis"

This reverts commit 38c438a614.

* Normalize SimpleFIN holding cost basis

* Track SimpleFIN cost basis source field

* Add SimpleFIN cost basis normalization tests

* Update SimpleFIN value cost basis expectation

* Handle missing SimpleFIN quantity for cost basis

* Ignore missing SimpleFIN cost basis fields

* Fix SimpleFIN holdings processor test setup
This commit is contained in:
Elvis De Abreu
2026-05-07 16:31:26 -05:00
committed by GitHub
parent 7e1de420ca
commit cf31c6e398
3 changed files with 125 additions and 3 deletions

View File

@@ -47,7 +47,8 @@ class SimplefinAccount::Investments::HoldingsProcessor
# which would cause the system to display average cost as current price. (GH #1182)
qty = parse_decimal(any_of(simplefin_holding, %w[shares quantity qty units]))
market_value = parse_decimal(any_of(simplefin_holding, %w[market_value current_value]))
cost_basis = parse_decimal(any_of(simplefin_holding, %w[cost_basis basis total_cost value]))
raw_cost_basis, cost_basis_source_key = cost_basis_from(simplefin_holding)
cost_basis = normalize_cost_basis(raw_cost_basis, qty, cost_basis_source_key)
# Derive price from market_value when possible; otherwise fall back to any price field
fallback_price = parse_decimal(any_of(simplefin_holding, %w[purchase_price price unit_price average_cost avg_cost]))
@@ -112,6 +113,32 @@ class SimplefinAccount::Investments::HoldingsProcessor
simplefin_account.raw_holdings_payload || []
end
def cost_basis_from(simplefin_holding)
%w[cost_basis basis total_cost value].each do |key|
raw = simplefin_holding[key]
next if raw.nil? || raw.to_s.strip.empty?
return [ parse_decimal(raw), key ]
end
[ nil, nil ]
end
# Sure stores holding cost_basis as per-share average cost. Some SimpleFIN
# providers expose total position basis via total_cost/value, so normalize only
# when the selected provider field is known to represent total position basis.
def normalize_cost_basis(raw_cost_basis, qty, source_key)
return nil if raw_cost_basis.nil?
if %w[total_cost value].include?(source_key)
return nil unless qty.to_d.positive?
raw_cost_basis / qty
else
raw_cost_basis
end
end
def resolve_security(symbol, description)
# Normalize crypto tickers to a distinct namespace so they don't collide with equities
sym = symbol.to_s.upcase