1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-05 13:35:21 +02:00

Basic Portfolio Views (#1000)

* Add holdings tab to account view

* Basic portfolio UI

* Cleanup

* Handle missing holding data

* Remove synced at (implemented in separate pr)

* translations

* Tweak post sync streams

* Remove stale methods from merge conflict
This commit is contained in:
Zach Gollwitzer 2024-07-25 16:46:04 -04:00 committed by GitHub
parent ef4be7948a
commit 7c2091b343
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 582 additions and 86 deletions

View file

@ -1,6 +1,46 @@
class Account::Holding < ApplicationRecord
include Monetizable
monetize :amount
belongs_to :account
belongs_to :security
validates :qty, :currency, presence: true
scope :chronological, -> { order(:date) }
scope :current, -> { where(date: Date.current).order(amount: :desc) }
scope :for, ->(security) { where(security_id: security).order(:date) }
delegate :name, to: :security
delegate :symbol, to: :security
def weight
return nil unless amount
portfolio_value = account.holdings.current.where.not(amount: nil).sum(&:amount)
portfolio_value.zero? ? 1 : amount / portfolio_value * 100
end
# Basic approximation of cost-basis
def avg_cost
avg_cost = account.holdings.for(security).where("date <= ?", date).average(:price)
Money.new(avg_cost, currency)
end
def trend
@trend ||= calculate_trend
end
private
def calculate_trend
return nil unless amount_money
start_amount = qty * avg_cost
TimeSeries::Trend.new \
current: amount_money,
previous: start_amount
end
end

View file

@ -38,14 +38,18 @@ class Account::Holding::Syncer
@portfolio = generate_next_portfolio(@portfolio, trades)
@portfolio.map do |isin, holding|
price = Security::Price.find_by!(date: date, isin: isin).price
trade = trades.find { |trade| trade.account_trade.security_id == holding[:security_id] }
trade_price = trade&.account_trade&.price
price = Security::Price.find_by(date: date, isin: isin)&.price || trade_price
account.holdings.build \
date: date,
security_id: holding[:security_id],
qty: holding[:qty],
price: price,
amount: price * holding[:qty]
amount: price ? (price * holding[:qty]) : nil,
currency: holding[:currency]
end
end
@ -61,6 +65,7 @@ class Account::Holding::Syncer
qty: new_qty,
price: price,
amount: new_qty * price,
currency: entry.currency,
security_id: trade.security_id
}
end
@ -85,6 +90,7 @@ class Account::Holding::Syncer
qty: holding.qty,
price: holding.price,
amount: holding.amount,
currency: holding.currency,
security_id: holding.security_id
}
end

View file

@ -78,6 +78,6 @@ class Account::Sync < ApplicationRecord
partial: "shared/notification",
locals: { type: type, message: message }
)
broadcast_refresh_to account
account.family.broadcast_refresh
end
end

View file

@ -165,6 +165,9 @@ class Demo::Generator
end
def load_securities!
# Create an unknown security to simulate edge cases
Security.create! isin: "unknown", symbol: "UNKNOWN", name: "Unknown Demo Stock"
securities = [
{ isin: "US0378331005", symbol: "AAPL", name: "Apple Inc.", reference_price: 210 },
{ isin: "JP3633400001", symbol: "TM", name: "Toyota Motor Corporation", reference_price: 202 },
@ -200,6 +203,10 @@ class Demo::Generator
aapl = Security.find_by(symbol: "AAPL")
tm = Security.find_by(symbol: "TM")
msft = Security.find_by(symbol: "MSFT")
unknown = Security.find_by(symbol: "UNKNOWN")
# Buy 20 shares of the unknown stock to simulate a stock where we can't fetch security prices
account.entries.create! date: 10.days.ago.to_date, amount: 100, currency: "USD", name: "Buy unknown stock", entryable: Account::Trade.new(qty: 20, price: 5, security: unknown)
trades = [
{ security: aapl, qty: 20 }, { security: msft, qty: 10 }, { security: aapl, qty: -5 },
@ -212,7 +219,7 @@ class Demo::Generator
date = Faker::Number.positive(to: 730).days.ago.to_date
security = trade[:security]
qty = trade[:qty]
price = Security::Price.find_by!(isin: security.isin, date: date).price
price = Security::Price.find_by(isin: security.isin, date: date)&.price || 1
name_prefix = qty < 0 ? "Sell " : "Buy "
account.entries.create! \

View file

@ -44,7 +44,7 @@ class TimeSeries::Trend
end
def percent
if previous.nil?
if previous.nil? || (previous.zero? && current.zero?)
0.0
elsif previous.zero?
Float::INFINITY