mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 15:35:22 +02:00
Flatten Holding model
This commit is contained in:
parent
48c8499b70
commit
21ed7d27d6
34 changed files with 149 additions and 142 deletions
|
@ -87,19 +87,19 @@ Below are examples of necessary vs. unnecessary tests:
|
|||
# GOOD!!
|
||||
# Necessary test - in this case, we're testing critical domain business logic
|
||||
test "syncs balances" do
|
||||
Account::Holding::Syncer.any_instance.expects(:sync_holdings).returns([]).once
|
||||
Holding::Syncer.any_instance.expects(:sync_holdings).returns([]).once
|
||||
|
||||
@account.expects(:start_date).returns(2.days.ago.to_date)
|
||||
|
||||
Account::Balance::ForwardCalculator.any_instance.expects(:calculate).returns(
|
||||
Balance::ForwardCalculator.any_instance.expects(:calculate).returns(
|
||||
[
|
||||
Account::Balance.new(date: 1.day.ago.to_date, balance: 1000, cash_balance: 1000, currency: "USD"),
|
||||
Account::Balance.new(date: Date.current, balance: 1000, cash_balance: 1000, currency: "USD")
|
||||
Balance.new(date: 1.day.ago.to_date, balance: 1000, cash_balance: 1000, currency: "USD"),
|
||||
Balance.new(date: Date.current, balance: 1000, cash_balance: 1000, currency: "USD")
|
||||
]
|
||||
)
|
||||
|
||||
assert_difference "@account.balances.count", 2 do
|
||||
Account::Balance::Syncer.new(@account, strategy: :forward).sync_balances
|
||||
Balance::Syncer.new(@account, strategy: :forward).sync_balances
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -55,13 +55,13 @@ All balances are calculated daily by [balance_calculator.rb](mdc:app/models/acco
|
|||
|
||||
### Account Holdings
|
||||
|
||||
An account [holding.rb](mdc:app/models/account/holding.rb) applies to [investment.rb](mdc:app/models/investment.rb) type accounts and represents a `qty` of a certain [security.rb](mdc:app/models/security.rb) at a specific `price` on a specific `date`.
|
||||
An account [holding.rb](mdc:app/models/holding.rb) applies to [investment.rb](mdc:app/models/investment.rb) type accounts and represents a `qty` of a certain [security.rb](mdc:app/models/security.rb) at a specific `price` on a specific `date`.
|
||||
|
||||
For investment accounts with holdings, [holding_calculator.rb](mdc:app/models/account/holding_calculator.rb) is used to calculate the daily historical holding quantities and prices, which are then rolled up into a final "Balance" for the account in [balance_calculator.rb](mdc:app/models/account/balance_calculator.rb).
|
||||
For investment accounts with holdings, [base_calculator.rb](mdc:app/models/holding/base_calculator.rb) is used to calculate the daily historical holding quantities and prices, which are then rolled up into a final "Balance" for the account in [base_calculator.rb](mdc:app/models/account/balance/base_calculator.rb).
|
||||
|
||||
### Account Entries
|
||||
|
||||
An account [entry.rb](mdc:app/models/account/entry.rb) is also a Rails "delegated type". `Account::Entry` represents any record that _modifies_ an `Account` [balance.rb](mdc:app/models/account/balance.rb) and/or [holding.rb](mdc:app/models/account/holding.rb). Therefore, every entry must have a `date`, `amount`, and `currency`.
|
||||
An account [entry.rb](mdc:app/models/account/entry.rb) is also a Rails "delegated type". `Account::Entry` represents any record that _modifies_ an `Account` [balance.rb](mdc:app/models/account/balance.rb) and/or [holding.rb](mdc:app/models/holding.rb). Therefore, every entry must have a `date`, `amount`, and `currency`.
|
||||
|
||||
The `amount` of an [entry.rb](mdc:app/models/account/entry.rb) is a signed value. A _negative_ amount is an "inflow" of money to that account. A _positive_ value is an "outflow" of money from that account. For example:
|
||||
|
||||
|
@ -115,7 +115,7 @@ The most important type of sync is the account sync. It is orchestrated by the
|
|||
|
||||
- Auto-matches transfer records for the account
|
||||
- Calculates daily [balance.rb](mdc:app/models/account/balance.rb) records for the account from `account.start_date` to `Date.current` using [base_calculator.rb](mdc:app/models/account/balance/base_calculator.rb)
|
||||
- Balances are dependent on the calculation of [holding.rb](mdc:app/models/account/holding.rb), which uses [base_calculator.rb](mdc:app/models/account/holding/base_calculator.rb)
|
||||
- Balances are dependent on the calculation of [holding.rb](mdc:app/models/holding.rb), which uses [base_calculator.rb](mdc:app/models/account/holding/base_calculator.rb)
|
||||
- Enriches transaction data if enabled by user
|
||||
|
||||
An account sync happens every time an [entry.rb](mdc:app/models/account/entry.rb) is updated.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class Account::HoldingsController < ApplicationController
|
||||
class HoldingsController < ApplicationController
|
||||
before_action :set_holding, only: %i[show destroy]
|
||||
|
||||
def index
|
|
@ -11,7 +11,7 @@ class Account < ApplicationRecord
|
|||
has_many :transactions, through: :entries, source: :entryable, source_type: "Account::Transaction"
|
||||
has_many :valuations, through: :entries, source: :entryable, source_type: "Account::Valuation"
|
||||
has_many :trades, through: :entries, source: :entryable, source_type: "Account::Trade"
|
||||
has_many :holdings, dependent: :destroy, class_name: "Account::Holding"
|
||||
has_many :holdings, dependent: :destroy
|
||||
has_many :balances, dependent: :destroy
|
||||
|
||||
monetize :balance, :cash_balance
|
||||
|
|
|
@ -26,7 +26,7 @@ class Account::Balance::Syncer
|
|||
|
||||
private
|
||||
def sync_holdings
|
||||
@holdings = Account::Holding::Syncer.new(account, strategy: strategy).sync_holdings
|
||||
@holdings = Holding::Syncer.new(account, strategy: strategy).sync_holdings
|
||||
end
|
||||
|
||||
def update_account_info
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class Account::Holding < ApplicationRecord
|
||||
class Holding < ApplicationRecord
|
||||
self.table_name = "account_holdings"
|
||||
|
||||
include Monetizable, Gapfillable
|
||||
|
||||
monetize :amount
|
|
@ -1,4 +1,4 @@
|
|||
class Account::Holding::BaseCalculator
|
||||
class Holding::BaseCalculator
|
||||
attr_reader :account
|
||||
|
||||
def initialize(account)
|
||||
|
@ -8,13 +8,13 @@ class Account::Holding::BaseCalculator
|
|||
def calculate
|
||||
Rails.logger.tagged(self.class.name) do
|
||||
holdings = calculate_holdings
|
||||
Account::Holding.gapfill(holdings)
|
||||
Holding.gapfill(holdings)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def portfolio_cache
|
||||
@portfolio_cache ||= Account::Holding::PortfolioCache.new(account)
|
||||
@portfolio_cache ||= Holding::PortfolioCache.new(account)
|
||||
end
|
||||
|
||||
def empty_portfolio
|
||||
|
@ -49,7 +49,7 @@ class Account::Holding::BaseCalculator
|
|||
next
|
||||
end
|
||||
|
||||
Account::Holding.new(
|
||||
Holding.new(
|
||||
account_id: account.id,
|
||||
security_id: security_id,
|
||||
date: date,
|
|
@ -1,7 +1,7 @@
|
|||
class Account::Holding::ForwardCalculator < Account::Holding::BaseCalculator
|
||||
class Holding::ForwardCalculator < Holding::BaseCalculator
|
||||
private
|
||||
def portfolio_cache
|
||||
@portfolio_cache ||= Account::Holding::PortfolioCache.new(account)
|
||||
@portfolio_cache ||= Holding::PortfolioCache.new(account)
|
||||
end
|
||||
|
||||
def calculate_holdings
|
|
@ -1,4 +1,4 @@
|
|||
module Account::Holding::Gapfillable
|
||||
module Holding::Gapfillable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
|
@ -19,7 +19,7 @@ module Account::Holding::Gapfillable
|
|||
previous_holding = holding
|
||||
else
|
||||
# Create a new holding based on the previous day's data
|
||||
filled_holdings << Account::Holding.new(
|
||||
filled_holdings << Holding.new(
|
||||
account: previous_holding.account,
|
||||
security: previous_holding.security,
|
||||
date: date,
|
|
@ -1,4 +1,4 @@
|
|||
class Account::Holding::PortfolioCache
|
||||
class Holding::PortfolioCache
|
||||
attr_reader :account, :use_holdings
|
||||
|
||||
class SecurityNotFound < StandardError
|
|
@ -1,10 +1,10 @@
|
|||
class Account::Holding::ReverseCalculator < Account::Holding::BaseCalculator
|
||||
class Holding::ReverseCalculator < Holding::BaseCalculator
|
||||
private
|
||||
# Reverse calculators will use the existing holdings as a source of security ids and prices
|
||||
# since it is common for a provider to supply "current day" holdings but not all the historical
|
||||
# trades that make up those holdings.
|
||||
def portfolio_cache
|
||||
@portfolio_cache ||= Account::Holding::PortfolioCache.new(account, use_holdings: true)
|
||||
@portfolio_cache ||= Holding::PortfolioCache.new(account, use_holdings: true)
|
||||
end
|
||||
|
||||
def calculate_holdings
|
|
@ -1,4 +1,4 @@
|
|||
class Account::Holding::Syncer
|
||||
class Holding::Syncer
|
||||
def initialize(account, strategy:)
|
||||
@account = account
|
||||
@strategy = strategy
|
||||
|
@ -50,9 +50,9 @@ class Account::Holding::Syncer
|
|||
|
||||
def calculator
|
||||
if strategy == :reverse
|
||||
Account::Holding::ReverseCalculator.new(account)
|
||||
Holding::ReverseCalculator.new(account)
|
||||
else
|
||||
Account::Holding::ForwardCalculator.new(account)
|
||||
Holding::ForwardCalculator.new(account)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@
|
|||
<%= image_tag "https://logo.synthfinance.com/ticker/#{holding.ticker}", class: "w-9 h-9 rounded-full", loading: "lazy" %>
|
||||
|
||||
<div class="space-y-0.5">
|
||||
<%= link_to holding.name, account_holding_path(holding), data: { turbo_frame: :drawer }, class: "hover:underline" %>
|
||||
<%= link_to holding.name, holding_path(holding), data: { turbo_frame: :drawer }, class: "hover:underline" %>
|
||||
|
||||
<% if holding.amount %>
|
||||
<%= tag.p holding.ticker, class: "text-secondary text-xs uppercase" %>
|
|
@ -21,12 +21,12 @@
|
|||
</div>
|
||||
|
||||
<div class="rounded-lg bg-container shadow-border-xs">
|
||||
<%= render "account/holdings/cash", account: @account %>
|
||||
<%= render "holdings/cash", account: @account %>
|
||||
|
||||
<%= render "account/holdings/ruler" %>
|
||||
<%= render "holdings/ruler" %>
|
||||
|
||||
<% if @account.current_holdings.any? %>
|
||||
<%= render partial: "account/holdings/holding", collection: @account.current_holdings, spacer_template: "ruler" %>
|
||||
<%= render partial: "holdings/holding", collection: @account.current_holdings, spacer_template: "ruler" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -102,7 +102,7 @@
|
|||
</div>
|
||||
|
||||
<%= button_to t(".delete"),
|
||||
account_holding_path(@holding),
|
||||
holding_path(@holding),
|
||||
method: :delete,
|
||||
class: "rounded-lg px-3 py-2 text-red-500 text-sm font-medium border border-secondary",
|
||||
data: { turbo_confirm: true } %>
|
|
@ -1,5 +1,5 @@
|
|||
<%# locals: (account:) %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(account, :holdings), src: account_holdings_path(account_id: account.id) do %>
|
||||
<%= turbo_frame_tag dom_id(account, :holdings), src: account_holdings_path(account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
---
|
||||
en:
|
||||
account:
|
||||
holdings:
|
||||
cash:
|
||||
brokerage_cash: Brokerage cash
|
||||
destroy:
|
||||
success: Holding deleted
|
||||
holding:
|
||||
per_share: per share
|
||||
shares: "%{qty} shares"
|
||||
index:
|
||||
cost: cost
|
||||
holdings: Holdings
|
||||
name: name
|
||||
new_holding: New transaction
|
||||
no_holdings: No holdings to show.
|
||||
return: total return
|
||||
weight: weight
|
||||
missing_price_tooltip:
|
||||
description: This investment has missing values and we could not calculate
|
||||
its returns or value.
|
||||
missing_data: Missing data
|
||||
show:
|
||||
avg_cost_label: Average Cost
|
||||
current_market_price_label: Current Market Price
|
||||
delete: Delete
|
||||
delete_subtitle: This will delete the holding and all your associated trades
|
||||
on this account. This action cannot be undone.
|
||||
delete_title: Delete holding
|
||||
history: History
|
||||
overview: Overview
|
||||
portfolio_weight_label: Portfolio Weight
|
||||
settings: Settings
|
||||
ticker_label: Ticker
|
||||
trade_history_entry: "%{qty} shares of %{security} at %{price}"
|
||||
trend_label: Trend
|
||||
unknown: Unknown
|
37
config/locales/views/holdings/en.yml
Normal file
37
config/locales/views/holdings/en.yml
Normal file
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
en:
|
||||
holdings:
|
||||
cash:
|
||||
brokerage_cash: Brokerage cash
|
||||
destroy:
|
||||
success: Holding deleted
|
||||
holding:
|
||||
per_share: per share
|
||||
shares: "%{qty} shares"
|
||||
index:
|
||||
cost: cost
|
||||
holdings: Holdings
|
||||
name: name
|
||||
new_holding: New transaction
|
||||
no_holdings: No holdings to show.
|
||||
return: total return
|
||||
weight: weight
|
||||
missing_price_tooltip:
|
||||
description: This investment has missing values and we could not calculate
|
||||
its returns or value.
|
||||
missing_data: Missing data
|
||||
show:
|
||||
avg_cost_label: Average Cost
|
||||
current_market_price_label: Current Market Price
|
||||
delete: Delete
|
||||
delete_subtitle: This will delete the holding and all your associated trades
|
||||
on this account. This action cannot be undone.
|
||||
delete_title: Delete holding
|
||||
history: History
|
||||
overview: Overview
|
||||
portfolio_weight_label: Portfolio Weight
|
||||
settings: Settings
|
||||
ticker_label: Ticker
|
||||
trade_history_entry: "%{qty} shares of %{security} at %{price}"
|
||||
trend_label: Trend
|
||||
unknown: Unknown
|
|
@ -105,13 +105,13 @@ Rails.application.routes.draw do
|
|||
get :chart
|
||||
get :sparkline
|
||||
end
|
||||
|
||||
resources :holdings, only: %i[index new show destroy], shallow: true
|
||||
end
|
||||
|
||||
resources :accountable_sparklines, only: :show, param: :accountable_type
|
||||
|
||||
namespace :account do
|
||||
resources :holdings, only: %i[index new show destroy]
|
||||
|
||||
resources :transactions, only: %i[show new create update destroy] do
|
||||
resource :transfer_match, only: %i[new create]
|
||||
resource :category, only: :update, controller: :transaction_categories
|
||||
|
|
|
@ -81,7 +81,7 @@ namespace :securities do
|
|||
puts " Duplicate without MIC: #{security.id}"
|
||||
|
||||
# Count affected records
|
||||
holdings_count = Account::Holding.where(security_id: security.id).count
|
||||
holdings_count = Holding.where(security_id: security.id).count
|
||||
trades_count = Account::Trade.where(security_id: security.id).count
|
||||
prices_count = Security::Price.where(security_id: security.id).count
|
||||
|
||||
|
@ -94,7 +94,7 @@ namespace :securities do
|
|||
begin
|
||||
ActiveRecord::Base.transaction do
|
||||
# Update all references to point to the canonical security
|
||||
Account::Holding.where(security_id: security.id).update_all(security_id: canonical.id)
|
||||
Holding.where(security_id: security.id).update_all(security_id: canonical.id)
|
||||
Account::Trade.where(security_id: security.id).update_all(security_id: canonical.id)
|
||||
Security::Price.where(security_id: security.id).update_all(security_id: canonical.id)
|
||||
|
||||
|
@ -134,7 +134,7 @@ namespace :securities do
|
|||
puts " Duplicates: #{duplicates.map(&:id).join(', ')}"
|
||||
|
||||
# Count affected records
|
||||
holdings_count = Account::Holding.where(security_id: duplicates).count
|
||||
holdings_count = Holding.where(security_id: duplicates).count
|
||||
trades_count = Account::Trade.where(security_id: duplicates).count
|
||||
prices_count = Security::Price.where(security_id: duplicates).count
|
||||
|
||||
|
@ -151,7 +151,7 @@ namespace :securities do
|
|||
begin
|
||||
ActiveRecord::Base.transaction do
|
||||
# Update all references to point to the canonical security
|
||||
Account::Holding.where(security_id: duplicates).update_all(security_id: canonical.id)
|
||||
Holding.where(security_id: duplicates).update_all(security_id: canonical.id)
|
||||
Account::Trade.where(security_id: duplicates).update_all(security_id: canonical.id)
|
||||
Security::Price.where(security_id: duplicates).update_all(security_id: canonical.id)
|
||||
|
||||
|
|
|
@ -1,27 +1,33 @@
|
|||
require "test_helper"
|
||||
|
||||
class Account::HoldingsControllerTest < ActionDispatch::IntegrationTest
|
||||
class HoldingsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in users(:family_admin)
|
||||
@account = accounts(:investment)
|
||||
@holding = @account.holdings.first
|
||||
end
|
||||
|
||||
test "gets index" do
|
||||
get account_holdings_url(@account)
|
||||
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "gets holdings" do
|
||||
get account_holdings_url(account_id: @account.id)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "gets holding" do
|
||||
get account_holding_path(@holding)
|
||||
get holding_path(@holding)
|
||||
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "destroys holding and associated entries" do
|
||||
assert_difference -> { Account::Holding.count } => -1,
|
||||
assert_difference -> { Holding.count } => -1,
|
||||
-> { Account::Entry.count } => -1 do
|
||||
delete account_holding_path(@holding)
|
||||
delete holding_path(@holding)
|
||||
end
|
||||
|
||||
assert_redirected_to account_path(@holding.account)
|
|
@ -64,7 +64,7 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
|
|||
|
||||
assert_not ExchangeRate.exists?(exchange_rate.id)
|
||||
assert_not Security::Price.exists?(security_price.id)
|
||||
assert_not Account::Holding.exists?(holding.id)
|
||||
assert_not Holding.exists?(holding.id)
|
||||
assert_not Account::Balance.exists?(account_balance.id)
|
||||
end
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ class Account::Balance::SyncerTest < ActiveSupport::TestCase
|
|||
end
|
||||
|
||||
test "syncs balances" do
|
||||
Account::Holding::Syncer.any_instance.expects(:sync_holdings).returns([]).once
|
||||
Holding::Syncer.any_instance.expects(:sync_holdings).returns([]).once
|
||||
|
||||
@account.expects(:start_date).returns(2.days.ago.to_date)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
require "test_helper"
|
||||
|
||||
class Account::Holding::ForwardCalculatorTest < ActiveSupport::TestCase
|
||||
class Holding::ForwardCalculatorTest < ActiveSupport::TestCase
|
||||
include Account::EntriesTestHelper
|
||||
|
||||
setup do
|
||||
|
@ -14,7 +14,7 @@ class Account::Holding::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
end
|
||||
|
||||
test "no holdings" do
|
||||
calculated = Account::Holding::ForwardCalculator.new(@account).calculate
|
||||
calculated = Holding::ForwardCalculator.new(@account).calculate
|
||||
assert_equal [], calculated
|
||||
end
|
||||
|
||||
|
@ -35,32 +35,32 @@ class Account::Holding::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
|
||||
expected = [
|
||||
# 4 days ago
|
||||
Account::Holding.new(security: @voo, date: 4.days.ago.to_date, qty: 0, price: 460, amount: 0),
|
||||
Account::Holding.new(security: @wmt, date: 4.days.ago.to_date, qty: 0, price: 100, amount: 0),
|
||||
Account::Holding.new(security: @amzn, date: 4.days.ago.to_date, qty: 0, price: 200, amount: 0),
|
||||
Holding.new(security: @voo, date: 4.days.ago.to_date, qty: 0, price: 460, amount: 0),
|
||||
Holding.new(security: @wmt, date: 4.days.ago.to_date, qty: 0, price: 100, amount: 0),
|
||||
Holding.new(security: @amzn, date: 4.days.ago.to_date, qty: 0, price: 200, amount: 0),
|
||||
|
||||
# 3 days ago
|
||||
Account::Holding.new(security: @voo, date: 3.days.ago.to_date, qty: 20, price: 470, amount: 9400),
|
||||
Account::Holding.new(security: @wmt, date: 3.days.ago.to_date, qty: 0, price: 100, amount: 0),
|
||||
Account::Holding.new(security: @amzn, date: 3.days.ago.to_date, qty: 0, price: 200, amount: 0),
|
||||
Holding.new(security: @voo, date: 3.days.ago.to_date, qty: 20, price: 470, amount: 9400),
|
||||
Holding.new(security: @wmt, date: 3.days.ago.to_date, qty: 0, price: 100, amount: 0),
|
||||
Holding.new(security: @amzn, date: 3.days.ago.to_date, qty: 0, price: 200, amount: 0),
|
||||
|
||||
# 2 days ago
|
||||
Account::Holding.new(security: @voo, date: 2.days.ago.to_date, qty: 5, price: 480, amount: 2400),
|
||||
Account::Holding.new(security: @wmt, date: 2.days.ago.to_date, qty: 0, price: 100, amount: 0),
|
||||
Account::Holding.new(security: @amzn, date: 2.days.ago.to_date, qty: 1, price: 200, amount: 200),
|
||||
Holding.new(security: @voo, date: 2.days.ago.to_date, qty: 5, price: 480, amount: 2400),
|
||||
Holding.new(security: @wmt, date: 2.days.ago.to_date, qty: 0, price: 100, amount: 0),
|
||||
Holding.new(security: @amzn, date: 2.days.ago.to_date, qty: 1, price: 200, amount: 200),
|
||||
|
||||
# 1 day ago
|
||||
Account::Holding.new(security: @voo, date: 1.day.ago.to_date, qty: 10, price: 490, amount: 4900),
|
||||
Account::Holding.new(security: @wmt, date: 1.day.ago.to_date, qty: 100, price: 100, amount: 10000),
|
||||
Account::Holding.new(security: @amzn, date: 1.day.ago.to_date, qty: 0, price: 200, amount: 0),
|
||||
Holding.new(security: @voo, date: 1.day.ago.to_date, qty: 10, price: 490, amount: 4900),
|
||||
Holding.new(security: @wmt, date: 1.day.ago.to_date, qty: 100, price: 100, amount: 10000),
|
||||
Holding.new(security: @amzn, date: 1.day.ago.to_date, qty: 0, price: 200, amount: 0),
|
||||
|
||||
# Today
|
||||
Account::Holding.new(security: @voo, date: Date.current, qty: 10, price: 500, amount: 5000),
|
||||
Account::Holding.new(security: @wmt, date: Date.current, qty: 100, price: 100, amount: 10000),
|
||||
Account::Holding.new(security: @amzn, date: Date.current, qty: 0, price: 200, amount: 0)
|
||||
Holding.new(security: @voo, date: Date.current, qty: 10, price: 500, amount: 5000),
|
||||
Holding.new(security: @wmt, date: Date.current, qty: 100, price: 100, amount: 10000),
|
||||
Holding.new(security: @amzn, date: Date.current, qty: 0, price: 200, amount: 0)
|
||||
]
|
||||
|
||||
calculated = Account::Holding::ForwardCalculator.new(@account).calculate
|
||||
calculated = Holding::ForwardCalculator.new(@account).calculate
|
||||
|
||||
assert_equal expected.length, calculated.length
|
||||
assert_holdings(expected, calculated)
|
||||
|
@ -74,9 +74,9 @@ class Account::Holding::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
create_trade(@wmt, qty: 100, date: 1.day.ago.to_date, price: 100, account: @account)
|
||||
|
||||
expected = [
|
||||
Account::Holding.new(security: @wmt, date: 2.days.ago.to_date, qty: 0, price: 100, amount: 0),
|
||||
Account::Holding.new(security: @wmt, date: 1.day.ago.to_date, qty: 100, price: 100, amount: 10000),
|
||||
Account::Holding.new(security: @wmt, date: Date.current, qty: 100, price: 100, amount: 10000)
|
||||
Holding.new(security: @wmt, date: 2.days.ago.to_date, qty: 0, price: 100, amount: 0),
|
||||
Holding.new(security: @wmt, date: 1.day.ago.to_date, qty: 100, price: 100, amount: 10000),
|
||||
Holding.new(security: @wmt, date: Date.current, qty: 100, price: 100, amount: 10000)
|
||||
]
|
||||
|
||||
# Price missing today, so we should carry forward the holding from 1 day ago
|
||||
|
@ -85,7 +85,7 @@ class Account::Holding::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
Security::Price.stubs(:find_price).with(security: @wmt, date: 1.day.ago.to_date).returns(Security::Price.new(price: 100))
|
||||
Security::Price.stubs(:find_price).with(security: @wmt, date: Date.current).returns(nil)
|
||||
|
||||
calculated = Account::Holding::ForwardCalculator.new(@account).calculate
|
||||
calculated = Holding::ForwardCalculator.new(@account).calculate
|
||||
|
||||
assert_equal expected.length, calculated.length
|
||||
assert_holdings(expected, calculated)
|
||||
|
@ -98,13 +98,13 @@ class Account::Holding::ForwardCalculatorTest < ActiveSupport::TestCase
|
|||
create_trade(offline_security, qty: 1, date: 1.day.ago.to_date, price: 100, account: @account)
|
||||
|
||||
expected = [
|
||||
Account::Holding.new(security: offline_security, date: 3.days.ago.to_date, qty: 1, price: 90, amount: 90),
|
||||
Account::Holding.new(security: offline_security, date: 2.days.ago.to_date, qty: 1, price: 90, amount: 90),
|
||||
Account::Holding.new(security: offline_security, date: 1.day.ago.to_date, qty: 2, price: 100, amount: 200),
|
||||
Account::Holding.new(security: offline_security, date: Date.current, qty: 2, price: 100, amount: 200)
|
||||
Holding.new(security: offline_security, date: 3.days.ago.to_date, qty: 1, price: 90, amount: 90),
|
||||
Holding.new(security: offline_security, date: 2.days.ago.to_date, qty: 1, price: 90, amount: 90),
|
||||
Holding.new(security: offline_security, date: 1.day.ago.to_date, qty: 2, price: 100, amount: 200),
|
||||
Holding.new(security: offline_security, date: Date.current, qty: 2, price: 100, amount: 200)
|
||||
]
|
||||
|
||||
calculated = Account::Holding::ForwardCalculator.new(@account).calculate
|
||||
calculated = Holding::ForwardCalculator.new(@account).calculate
|
||||
|
||||
assert_equal expected.length, calculated.length
|
||||
assert_holdings(expected, calculated)
|
|
@ -1,6 +1,6 @@
|
|||
require "test_helper"
|
||||
|
||||
class Account::Holding::PortfolioCacheTest < ActiveSupport::TestCase
|
||||
class Holding::PortfolioCacheTest < ActiveSupport::TestCase
|
||||
include Account::EntriesTestHelper, ProviderTestHelper
|
||||
|
||||
setup do
|
||||
|
@ -30,7 +30,7 @@ class Account::Holding::PortfolioCacheTest < ActiveSupport::TestCase
|
|||
|
||||
expect_provider_prices([], start_date: @account.start_date)
|
||||
|
||||
cache = Account::Holding::PortfolioCache.new(@account)
|
||||
cache = Holding::PortfolioCache.new(@account)
|
||||
assert_equal db_price, cache.get_price(@security.id, Date.current).price
|
||||
end
|
||||
|
||||
|
@ -46,7 +46,7 @@ class Account::Holding::PortfolioCacheTest < ActiveSupport::TestCase
|
|||
|
||||
expect_provider_prices([ provider_price ], start_date: @account.start_date)
|
||||
|
||||
cache = Account::Holding::PortfolioCache.new(@account)
|
||||
cache = Holding::PortfolioCache.new(@account)
|
||||
assert_equal provider_price.price, cache.get_price(@security.id, Date.current).price
|
||||
end
|
||||
|
||||
|
@ -54,7 +54,7 @@ class Account::Holding::PortfolioCacheTest < ActiveSupport::TestCase
|
|||
Security::Price.destroy_all
|
||||
expect_provider_prices([], start_date: @account.start_date)
|
||||
|
||||
cache = Account::Holding::PortfolioCache.new(@account)
|
||||
cache = Holding::PortfolioCache.new(@account)
|
||||
assert_equal @trade.price, cache.get_price(@security.id, @trade.entry.date).price
|
||||
end
|
||||
|
||||
|
@ -62,7 +62,7 @@ class Account::Holding::PortfolioCacheTest < ActiveSupport::TestCase
|
|||
Security::Price.delete_all
|
||||
Account::Entry.delete_all
|
||||
|
||||
holding = Account::Holding.create!(
|
||||
holding = Holding.create!(
|
||||
security: @security,
|
||||
account: @account,
|
||||
date: Date.current,
|
||||
|
@ -74,7 +74,7 @@ class Account::Holding::PortfolioCacheTest < ActiveSupport::TestCase
|
|||
|
||||
expect_provider_prices([], start_date: @account.start_date)
|
||||
|
||||
cache = Account::Holding::PortfolioCache.new(@account, use_holdings: true)
|
||||
cache = Holding::PortfolioCache.new(@account, use_holdings: true)
|
||||
assert_equal holding.price, cache.get_price(@security.id, holding.date).price
|
||||
end
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
require "test_helper"
|
||||
|
||||
class Account::Holding::ReverseCalculatorTest < ActiveSupport::TestCase
|
||||
class Holding::ReverseCalculatorTest < ActiveSupport::TestCase
|
||||
include Account::EntriesTestHelper
|
||||
|
||||
setup do
|
||||
|
@ -14,7 +14,7 @@ class Account::Holding::ReverseCalculatorTest < ActiveSupport::TestCase
|
|||
end
|
||||
|
||||
test "no holdings" do
|
||||
calculated = Account::Holding::ReverseCalculator.new(@account).calculate
|
||||
calculated = Holding::ReverseCalculator.new(@account).calculate
|
||||
assert_equal [], calculated
|
||||
end
|
||||
|
||||
|
@ -26,7 +26,7 @@ class Account::Holding::ReverseCalculatorTest < ActiveSupport::TestCase
|
|||
|
||||
create_trade(voo, qty: -10, date: Date.current, price: 470, account: @account)
|
||||
|
||||
calculated = Account::Holding::ReverseCalculator.new(@account).calculate
|
||||
calculated = Holding::ReverseCalculator.new(@account).calculate
|
||||
assert_equal 2, calculated.length
|
||||
end
|
||||
|
||||
|
@ -47,32 +47,32 @@ class Account::Holding::ReverseCalculatorTest < ActiveSupport::TestCase
|
|||
|
||||
expected = [
|
||||
# 4 days ago
|
||||
Account::Holding.new(security: @voo, date: 4.days.ago.to_date, qty: 0, price: 460, amount: 0),
|
||||
Account::Holding.new(security: @wmt, date: 4.days.ago.to_date, qty: 0, price: 100, amount: 0),
|
||||
Account::Holding.new(security: @amzn, date: 4.days.ago.to_date, qty: 0, price: 200, amount: 0),
|
||||
Holding.new(security: @voo, date: 4.days.ago.to_date, qty: 0, price: 460, amount: 0),
|
||||
Holding.new(security: @wmt, date: 4.days.ago.to_date, qty: 0, price: 100, amount: 0),
|
||||
Holding.new(security: @amzn, date: 4.days.ago.to_date, qty: 0, price: 200, amount: 0),
|
||||
|
||||
# 3 days ago
|
||||
Account::Holding.new(security: @voo, date: 3.days.ago.to_date, qty: 20, price: 470, amount: 9400),
|
||||
Account::Holding.new(security: @wmt, date: 3.days.ago.to_date, qty: 0, price: 100, amount: 0),
|
||||
Account::Holding.new(security: @amzn, date: 3.days.ago.to_date, qty: 0, price: 200, amount: 0),
|
||||
Holding.new(security: @voo, date: 3.days.ago.to_date, qty: 20, price: 470, amount: 9400),
|
||||
Holding.new(security: @wmt, date: 3.days.ago.to_date, qty: 0, price: 100, amount: 0),
|
||||
Holding.new(security: @amzn, date: 3.days.ago.to_date, qty: 0, price: 200, amount: 0),
|
||||
|
||||
# 2 days ago
|
||||
Account::Holding.new(security: @voo, date: 2.days.ago.to_date, qty: 5, price: 480, amount: 2400),
|
||||
Account::Holding.new(security: @wmt, date: 2.days.ago.to_date, qty: 0, price: 100, amount: 0),
|
||||
Account::Holding.new(security: @amzn, date: 2.days.ago.to_date, qty: 1, price: 200, amount: 200),
|
||||
Holding.new(security: @voo, date: 2.days.ago.to_date, qty: 5, price: 480, amount: 2400),
|
||||
Holding.new(security: @wmt, date: 2.days.ago.to_date, qty: 0, price: 100, amount: 0),
|
||||
Holding.new(security: @amzn, date: 2.days.ago.to_date, qty: 1, price: 200, amount: 200),
|
||||
|
||||
# 1 day ago
|
||||
Account::Holding.new(security: @voo, date: 1.day.ago.to_date, qty: 10, price: 490, amount: 4900),
|
||||
Account::Holding.new(security: @wmt, date: 1.day.ago.to_date, qty: 100, price: 100, amount: 10000),
|
||||
Account::Holding.new(security: @amzn, date: 1.day.ago.to_date, qty: 0, price: 200, amount: 0),
|
||||
Holding.new(security: @voo, date: 1.day.ago.to_date, qty: 10, price: 490, amount: 4900),
|
||||
Holding.new(security: @wmt, date: 1.day.ago.to_date, qty: 100, price: 100, amount: 10000),
|
||||
Holding.new(security: @amzn, date: 1.day.ago.to_date, qty: 0, price: 200, amount: 0),
|
||||
|
||||
# Today
|
||||
Account::Holding.new(security: @voo, date: Date.current, qty: 10, price: 500, amount: 5000),
|
||||
Account::Holding.new(security: @wmt, date: Date.current, qty: 100, price: 100, amount: 10000),
|
||||
Account::Holding.new(security: @amzn, date: Date.current, qty: 0, price: 200, amount: 0)
|
||||
Holding.new(security: @voo, date: Date.current, qty: 10, price: 500, amount: 5000),
|
||||
Holding.new(security: @wmt, date: Date.current, qty: 100, price: 100, amount: 10000),
|
||||
Holding.new(security: @amzn, date: Date.current, qty: 0, price: 200, amount: 0)
|
||||
]
|
||||
|
||||
calculated = Account::Holding::ReverseCalculator.new(@account).calculate
|
||||
calculated = Holding::ReverseCalculator.new(@account).calculate
|
||||
|
||||
assert_equal expected.length, calculated.length
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
require "test_helper"
|
||||
|
||||
class Account::Holding::SyncerTest < ActiveSupport::TestCase
|
||||
class Holding::SyncerTest < ActiveSupport::TestCase
|
||||
include Account::EntriesTestHelper
|
||||
|
||||
setup do
|
||||
|
@ -14,16 +14,16 @@ class Account::Holding::SyncerTest < ActiveSupport::TestCase
|
|||
|
||||
# Should have yesterday's and today's holdings
|
||||
assert_difference "@account.holdings.count", 2 do
|
||||
Account::Holding::Syncer.new(@account, strategy: :forward).sync_holdings
|
||||
Holding::Syncer.new(@account, strategy: :forward).sync_holdings
|
||||
end
|
||||
end
|
||||
|
||||
test "purges stale holdings for unlinked accounts" do
|
||||
# Since the account has no entries, there should be no holdings
|
||||
Account::Holding.create!(account: @account, security: @aapl, qty: 1, price: 100, amount: 100, currency: "USD", date: Date.current)
|
||||
Holding.create!(account: @account, security: @aapl, qty: 1, price: 100, amount: 100, currency: "USD", date: Date.current)
|
||||
|
||||
assert_difference "Account::Holding.count", -1 do
|
||||
Account::Holding::Syncer.new(@account, strategy: :forward).sync_holdings
|
||||
assert_difference "Holding.count", -1 do
|
||||
Holding::Syncer.new(@account, strategy: :forward).sync_holdings
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
require "test_helper"
|
||||
require "ostruct"
|
||||
|
||||
class Account::HoldingTest < ActiveSupport::TestCase
|
||||
class HoldingTest < ActiveSupport::TestCase
|
||||
include Account::EntriesTestHelper, SecuritiesTestHelper
|
||||
|
||||
setup do
|
|
@ -45,7 +45,7 @@ class PlaidInvestmentSyncTest < ActiveSupport::TestCase
|
|||
# Cash holding should be ignored, resulting in 1, NOT 2 total holdings after sync
|
||||
assert_difference -> { Account::Trade.count } => 1,
|
||||
-> { Account::Transaction.count } => 0,
|
||||
-> { Account::Holding.count } => 1,
|
||||
-> { Holding.count } => 1,
|
||||
-> { Security.count } => 0 do
|
||||
PlaidInvestmentSync.new(@plaid_account).sync!(
|
||||
transactions: transactions,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue