mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-07 14:35:23 +02:00
Handle holding quantity generation for reverse syncs correctly when not all holdings are generated for current day (#2417)
* Handle reverse calculator starting portfolio generation correctly * Fix current_holdings to handle different dates and hide zero quantities - Use DISTINCT ON to get most recent holding per security instead of assuming same date - Filter out zero quantity holdings from UI display - Maintain cash display regardless of zero balance - Use single efficient query with proper Rails syntax * Continue to process holdings even if one is not resolvable * Lint fixes
This commit is contained in:
parent
e60b5df442
commit
8db95623cf
8 changed files with 281 additions and 39 deletions
50
test/models/holding/portfolio_snapshot_test.rb
Normal file
50
test/models/holding/portfolio_snapshot_test.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
require "test_helper"
|
||||
|
||||
class Holding::PortfolioSnapshotTest < ActiveSupport::TestCase
|
||||
include EntriesTestHelper
|
||||
setup do
|
||||
@account = accounts(:investment)
|
||||
@aapl = securities(:aapl)
|
||||
@msft = securities(:msft)
|
||||
end
|
||||
|
||||
test "captures the most recent holding quantities for each security" do
|
||||
# Clear any existing data
|
||||
@account.holdings.destroy_all
|
||||
@account.entries.destroy_all
|
||||
|
||||
# Create some trades to establish which securities are in the portfolio
|
||||
create_trade(@aapl, account: @account, qty: 10, price: 100, date: 5.days.ago)
|
||||
create_trade(@msft, account: @account, qty: 30, price: 200, date: 5.days.ago)
|
||||
|
||||
# Create holdings for AAPL at different dates
|
||||
@account.holdings.create!(security: @aapl, date: 3.days.ago, qty: 10, price: 100, amount: 1000, currency: "USD")
|
||||
@account.holdings.create!(security: @aapl, date: 1.day.ago, qty: 20, price: 150, amount: 3000, currency: "USD")
|
||||
|
||||
# Create holdings for MSFT at different dates
|
||||
@account.holdings.create!(security: @msft, date: 5.days.ago, qty: 30, price: 200, amount: 6000, currency: "USD")
|
||||
@account.holdings.create!(security: @msft, date: 2.days.ago, qty: 40, price: 250, amount: 10000, currency: "USD")
|
||||
|
||||
snapshot = Holding::PortfolioSnapshot.new(@account)
|
||||
portfolio = snapshot.to_h
|
||||
|
||||
assert_equal 2, portfolio.size
|
||||
assert_equal 20, portfolio[@aapl.id]
|
||||
assert_equal 40, portfolio[@msft.id]
|
||||
end
|
||||
|
||||
test "includes securities from trades with zero quantities when no holdings exist" do
|
||||
# Clear any existing data
|
||||
@account.holdings.destroy_all
|
||||
@account.entries.destroy_all
|
||||
|
||||
# Create a trade to establish AAPL is in the portfolio
|
||||
create_trade(@aapl, account: @account, qty: 10, price: 100, date: 5.days.ago)
|
||||
|
||||
snapshot = Holding::PortfolioSnapshot.new(@account)
|
||||
portfolio = snapshot.to_h
|
||||
|
||||
assert_equal 1, portfolio.size
|
||||
assert_equal 0, portfolio[@aapl.id]
|
||||
end
|
||||
end
|
|
@ -14,7 +14,8 @@ class Holding::ReverseCalculatorTest < ActiveSupport::TestCase
|
|||
end
|
||||
|
||||
test "no holdings" do
|
||||
calculated = Holding::ReverseCalculator.new(@account).calculate
|
||||
empty_snapshot = OpenStruct.new(to_h: {})
|
||||
calculated = Holding::ReverseCalculator.new(@account, portfolio_snapshot: empty_snapshot).calculate
|
||||
assert_equal [], calculated
|
||||
end
|
||||
|
||||
|
@ -36,7 +37,9 @@ class Holding::ReverseCalculatorTest < ActiveSupport::TestCase
|
|||
create_trade(voo, qty: 10, date: "2025-01-03", price: 500, account: @account)
|
||||
|
||||
expected = [ [ "2025-01-02", 0 ], [ "2025-01-03", 5000 ], [ "2025-01-04", 5000 ] ]
|
||||
calculated = Holding::ReverseCalculator.new(@account).calculate
|
||||
# Mock snapshot with the holdings we created
|
||||
snapshot = OpenStruct.new(to_h: { voo.id => 10 })
|
||||
calculated = Holding::ReverseCalculator.new(@account, portfolio_snapshot: snapshot).calculate
|
||||
|
||||
assert_equal expected, calculated.sort_by(&:date).map { |b| [ b.date.to_s, b.amount ] }
|
||||
end
|
||||
|
@ -50,7 +53,9 @@ class Holding::ReverseCalculatorTest < ActiveSupport::TestCase
|
|||
|
||||
create_trade(voo, qty: -10, date: Date.current, price: 470, account: @account)
|
||||
|
||||
calculated = Holding::ReverseCalculator.new(@account).calculate
|
||||
# Mock empty portfolio since no current day holdings
|
||||
snapshot = OpenStruct.new(to_h: { voo.id => 0 })
|
||||
calculated = Holding::ReverseCalculator.new(@account, portfolio_snapshot: snapshot).calculate
|
||||
assert_equal 2, calculated.length
|
||||
end
|
||||
|
||||
|
@ -96,7 +101,9 @@ class Holding::ReverseCalculatorTest < ActiveSupport::TestCase
|
|||
Holding.new(security: @amzn, date: Date.current, qty: 0, price: 200, amount: 0)
|
||||
]
|
||||
|
||||
calculated = Holding::ReverseCalculator.new(@account).calculate
|
||||
# Mock snapshot with today's portfolio from load_today_portfolio
|
||||
snapshot = OpenStruct.new(to_h: { @voo.id => 10, @wmt.id => 100, @amzn.id => 0 })
|
||||
calculated = Holding::ReverseCalculator.new(@account, portfolio_snapshot: snapshot).calculate
|
||||
|
||||
assert_equal expected.length, calculated.length
|
||||
|
||||
|
@ -136,7 +143,9 @@ class Holding::ReverseCalculatorTest < ActiveSupport::TestCase
|
|||
Holding.new(security: wmt, date: Date.current, qty: 50, price: 100, amount: 5000) # Uses holding price, not market price
|
||||
]
|
||||
|
||||
calculated = Holding::ReverseCalculator.new(@account).calculate
|
||||
# Mock snapshot with WMT holding from the test setup
|
||||
snapshot = OpenStruct.new(to_h: { wmt.id => 50 })
|
||||
calculated = Holding::ReverseCalculator.new(@account, portfolio_snapshot: snapshot).calculate
|
||||
|
||||
assert_equal expected.length, calculated.length
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue