Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic trade and holdings view #1271

Merged
merged 4 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/controllers/account/entries_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def show
def destroy
@entry.destroy!
@entry.sync_account_later
redirect_back_or_to account_url(@entry.account), notice: t(".success")
redirect_to account_url(@entry.account), notice: t(".success")
end

private
Expand Down
7 changes: 6 additions & 1 deletion app/controllers/account/holdings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class Account::HoldingsController < ApplicationController
layout :with_sidebar

before_action :set_account
before_action :set_holding, only: :show
before_action :set_holding, only: %i[show destroy]

def index
@holdings = @account.holdings.current
Expand All @@ -11,6 +11,11 @@ def index
def show
end

def destroy
@holding.destroy_holding_and_entries!
redirect_back_or_to account_holdings_path(@account)
end

private

def set_account
Expand Down
24 changes: 23 additions & 1 deletion app/controllers/account/trades_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class Account::TradesController < ApplicationController
layout :with_sidebar

before_action :set_account
before_action :set_entry, only: :update

def new
@entry = @account.entries.account_trades.new(entryable_attributes: {})
Expand All @@ -23,15 +24,36 @@ def create
end
end

def update
@entry.update!(entry_params)

respond_to do |format|
format.html { redirect_to account_entry_path(@account, @entry), notice: t(".success") }
format.turbo_stream { render turbo_stream: turbo_stream.replace(@entry) }
end
end

private

def set_account
@account = Current.family.accounts.find(params[:account_id])
end

def set_entry
@entry = @account.entries.find(params[:id])
end

def entry_params
params.require(:account_entry)
.permit(:type, :date, :qty, :ticker, :price, :amount, :currency, :transfer_account_id)
.permit(
:type, :date, :qty, :ticker, :price, :amount, :notes, :excluded, :currency, :transfer_account_id, :entryable_type,
entryable_attributes: [
:id,
:qty,
:ticker,
:price
]
)
.merge(account: @account)
end
end
4 changes: 1 addition & 3 deletions app/controllers/account/transactions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,9 @@ def set_entry
def entry_params
params.require(:account_entry)
.permit(
:name, :date, :amount, :currency, :entryable_type, :nature,
:name, :date, :amount, :currency, :excluded, :notes, :entryable_type, :nature,
entryable_attributes: [
:id,
:notes,
:excluded,
:category_id,
:merchant_id,
{ tag_ids: [] }
Expand Down
4 changes: 2 additions & 2 deletions app/helpers/styled_form_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ def collection_select(method, collection, value_method, text_method, options = {
build_styled_field(label, field, options, remove_padding_right: true)
end

def money_field(amount_method, currency_method, options = {})
def money_field(amount_method, options = {})
@template.render partial: "shared/money_field", locals: {
form: self,
amount_method:,
currency_method:,
currency_method: options[:currency_method] || :currency,
**options
}
end
Expand Down
2 changes: 1 addition & 1 deletion app/models/account/entry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ def mark_transfers!
def bulk_update!(bulk_update_params)
bulk_attributes = {
date: bulk_update_params[:date],
notes: bulk_update_params[:notes],
entryable_attributes: {
notes: bulk_update_params[:notes],
category_id: bulk_update_params[:category_id],
merchant_id: bulk_update_params[:merchant_id]
}.compact_blank
Expand Down
13 changes: 13 additions & 0 deletions app/models/account/holding.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ def trend
@trend ||= calculate_trend
end

def trades
account.entries.where(entryable: account.trades.where(security: security)).reverse_chronological
end

def destroy_holding_and_entries!
transaction do
account.entries.where(entryable: account.trades.where(security: security)).destroy_all
destroy
end

account.sync_later
end

private

def calculate_trend
Expand Down
11 changes: 11 additions & 0 deletions app/models/account/trade.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,15 @@ def sell?
def buy?
qty > 0
end

def unrealized_gain_loss
return nil if sell?
current_price = security.current_price
return nil if current_price.nil?

current_value = current_price * qty.abs
cost_basis = price_money * qty.abs

TimeSeries::Trend.new(current: current_value, previous: cost_basis)
end
end
3 changes: 2 additions & 1 deletion app/models/mint_import.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def import!
amount: row.signed_amount,
name: row.name,
currency: row.currency,
entryable: Account::Transaction.new(category: category, tags: tags, notes: row.notes),
notes: row.notes,
entryable: Account::Transaction.new(category: category, tags: tags),
import: self

entry.save!
Expand Down
6 changes: 6 additions & 0 deletions app/models/security.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ class Security < ApplicationRecord

validates :ticker, presence: true, uniqueness: { case_sensitive: false }

def current_price
@current_price ||= Security::Price.find_price(ticker:, date: Date.current)
return nil if @current_price.nil?
Money.new(@current_price.price, @current_price.currency)
end

private

def upcase_ticker
Expand Down
3 changes: 2 additions & 1 deletion app/models/transaction_import.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ def import!
amount: row.signed_amount,
name: row.name,
currency: row.currency,
entryable: Account::Transaction.new(category: category, tags: tags, notes: row.notes),
notes: row.notes,
entryable: Account::Transaction.new(category: category, tags: tags),
import: self

entry.save!
Expand Down
85 changes: 76 additions & 9 deletions app/views/account/holdings/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,103 @@
<%= render "shared/circle_logo", name: @holding.name %>
</header>

<details class="group space-y-2">
<details class="group space-y-2" open>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
<h4><%= t(".overview") %></h4>
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
</summary>

<div>
<p class="pl-4 text-gray-500">Coming soon...</p>
<div class="pb-4">
<dl class="space-y-3 px-3 py-2">
<div class="flex items-center justify-between text-sm">
<dt class="text-gray-500"><%= t(".ticker_label") %></dt>
<dd class="text-gray-900"><%= @holding.ticker %></dd>
</div>

<div class="flex items-center justify-between text-sm">
<dt class="text-gray-500"><%= t(".current_market_price_label") %></dt>
<dd class="text-gray-900"><%= @holding.security.current_price ? format_money(@holding.security.current_price) : t(".unknown") %></dd>
</div>

<div class="flex items-center justify-between text-sm">
<dt class="text-gray-500"><%= t(".portfolio_weight_label") %></dt>
<dd class="text-gray-900"><%= @holding.weight ? number_to_percentage(@holding.weight, precision: 2) : t(".unknown") %></dd>
</div>

<div class="flex items-center justify-between text-sm">
<dt class="text-gray-500"><%= t(".avg_cost_label") %></dt>
<dd class="text-gray-900"><%= @holding.avg_cost ? format_money(@holding.avg_cost) : t(".unknown") %></dd>
</div>

<div class="flex items-center justify-between text-sm">
<dt class="text-gray-500"><%= t(".trend_label") %></dt>
<dd style="color: <%= @holding.trend&.color %>;">
<%= @holding.trend ? render("shared/trend_change", trend: @holding.trend) : t(".unknown") %>
</dd>
</div>
</dl>
</div>
</details>

<details class="group space-y-2">
<details class="group space-y-2" open>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
<h4><%= t(".history") %></h4>
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
</summary>

<div>
<p class="pl-4 text-gray-500">Coming soon...</p>
<div class="space-y-2">
<div class="px-3 py-4">
<% if @holding.trades.any? %>
<ul class="space-y-2">
<% @holding.trades.each_with_index do |trade_entry, index| %>
<li class="flex gap-4 text-sm space-y-1">
<div class="flex flex-col items-center gap-1.5 pt-2">
<div class="rounded-full h-1.5 w-1.5 bg-gray-300"></div>
<% unless index == @holding.trades.length - 1 %>
<div class="h-12 w-px bg-alpha-black-200"></div>
<% end %>
</div>

<div>
<p class="text-gray-500 text-xs uppercase"><%= l(trade_entry.date, format: :long) %></p>

<p><%= t(
".trade_history_entry",
qty: trade_entry.account_trade.qty,
security: trade_entry.account_trade.security.ticker,
price: format_money(trade_entry.account_trade.price)
) %></p>
</div>
</li>
<% end %>
</ul>

<% else %>
<p class="text-gray-500">No trade history available for this holding.</p>
<% end %>
</div>
</div>
</details>

<details class="group space-y-2">
<details class="group space-y-2" open>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
<h4><%= t(".settings") %></h4>
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
</summary>

<div>
<p class="pl-4 text-gray-500">Coming soon...</p>
<div class="pb-4">
<div class="flex items-center justify-between gap-2 p-3">
<div class="text-sm space-y-1">
<h4 class="text-gray-900"><%= t(".delete_title") %></h4>
<p class="text-gray-500"><%= t(".delete_subtitle") %></p>
</div>

<%= button_to t(".delete"),
account_holding_path(@holding.account, @holding),
method: :delete,
class: "rounded-lg px-3 py-2 text-red-500 text-sm font-medium border border-alpha-black-200",
data: { turbo_confirm: true, turbo_frame: "_top" } %>
</div>
</div>
</details>
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/views/account/trades/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<%= form.date_field :date, label: true %>

<div data-trade-form-target="amountInput" hidden>
<%= form.money_field :amount, :currency, label: t(".amount"), disable_currency: true %>
<%= form.money_field :amount, label: t(".amount"), disable_currency: true %>
</div>

<div data-trade-form-target="transferAccountInput" hidden>
Expand All @@ -25,7 +25,7 @@
</div>

<div data-trade-form-target="priceInput">
<%= form.money_field :price, :currency, label: t(".price"), disable_currency: true %>
<%= form.money_field :price, label: t(".price"), disable_currency: true %>
</div>
</div>

Expand Down
Loading