Skip to content

Commit

Permalink
Stock filter (#1376)
Browse files Browse the repository at this point in the history
* Initial pass at stock filtering

* Rough in filter

* Cleaning up security listing

* Tweak to search function

* Combobox tweaks

* Clean up search query

* Update trades test with combobox

* Update securities.yml
  • Loading branch information
Shpigford authored Oct 28, 2024
1 parent c2561b5 commit 7d8028b
Show file tree
Hide file tree
Showing 15 changed files with 104 additions and 7 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ gem "lucide-rails", github: "maybe-finance/lucide-rails"
# Hotwire
gem "stimulus-rails"
gem "turbo-rails"
gem "hotwire_combobox"

# Background Jobs
gem "good_job"
Expand Down
5 changes: 5 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ GEM
actioncable (>= 6.0.0)
listen (>= 3.0.0)
railties (>= 6.0.0)
hotwire_combobox (0.3.2)
rails (>= 7.0.7.2)
stimulus-rails (>= 1.2)
turbo-rails (>= 1.2)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
i18n-tasks (1.0.14)
Expand Down Expand Up @@ -485,6 +489,7 @@ DEPENDENCIES
good_job
holidays
hotwire-livereload
hotwire_combobox
i18n-tasks
image_processing (>= 1.2)
importmap-rails
Expand Down
30 changes: 29 additions & 1 deletion app/assets/stylesheets/application.tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
@apply focus-within:border-gray-900 focus-within:shadow-none focus-within:ring-4 focus-within:ring-gray-100;
}

.form-field__label {

.form-field__label, .hw-combobox__label {
@apply block text-xs text-gray-500 peer-disabled:text-gray-400;
}

Expand Down Expand Up @@ -120,6 +121,33 @@
}
}

.combobox {
.hw-combobox__main__wrapper, .hw-combobox__input {
@apply w-full;
}

.hw-combobox__main__wrapper {
@apply border-0 p-0 focus:border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none focus-within:shadow-none;
}

.hw-combobox__listbox {
@apply absolute top-[160%] right-0 w-full bg-transparent rounded z-30;
}

.hw_combobox__pagination__wrapper {
@apply h-px;

&:only-child {
@apply bg-transparent;
}
}

--hw-border-color: rgba(0, 0, 0, 0.2);
--hw-handle-width: 20px;
--hw-handle-height: 20px;
--hw-handle-offset-right: 0px;
}

/* Small, single purpose classes that should take precedence over other styles */
@layer utilities {
.scrollbar::-webkit-scrollbar {
Expand Down
4 changes: 4 additions & 0 deletions app/controllers/account/trades_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ def update
end
end

def securities
@pagy, @securities = pagy(Security.order(:name).search(params[:q]), limit: 20)
end

private

def set_account
Expand Down
1 change: 1 addition & 0 deletions app/models/account/trade_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def create_entry
end

def security
return Security.find(ticker) if ticker.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
Security.find_or_create_by(ticker: ticker)
end

Expand Down
24 changes: 24 additions & 0 deletions app/models/security.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,36 @@ class Security < ApplicationRecord
validates :ticker, presence: true
validates :ticker, uniqueness: { scope: :exchange_mic, case_sensitive: false }

scope :search, ->(query) {
return none if query.blank? || query.length < 2

# Clean and normalize the search terms
sanitized_query = query.split.map do |term|
cleaned_term = term.gsub(/[^a-zA-Z0-9]/, " ").strip
next if cleaned_term.blank?
cleaned_term
end.compact.join(" | ")

return none if sanitized_query.blank?

sanitized_query = ActiveRecord::Base.connection.quote(sanitized_query)

where("search_vector @@ to_tsquery('simple', #{sanitized_query}) AND exchange_mic IS NOT NULL")
.select("securities.*, ts_rank_cd(search_vector, to_tsquery('simple', #{sanitized_query})) AS rank")
.reorder("rank DESC")
}

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

def to_combobox_display
"#{ticker} - #{name} (#{exchange_acronym})"
end


private

def upcase_ticker
Expand Down
6 changes: 4 additions & 2 deletions app/views/account/trades/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
<div class="space-y-2">
<%= form.select :type, options_for_select([%w[Buy buy], %w[Sell sell], %w[Deposit transfer_in], %w[Withdrawal transfer_out], %w[Interest interest]], "buy"), { label: t(".type") }, { data: { "trade-form-target": "typeInput" } } %>
<div data-trade-form-target="tickerInput">
<%= form.text_field :ticker, value: nil, label: t(".holding"), placeholder: t(".ticker_placeholder") %>
<div class="form-field combobox">
<%= form.combobox :ticker, securities_account_trades_path(entry.account), label: t(".holding"), placeholder: t(".ticker_placeholder"), autocomplete: :list, free_text: true %>
</div>
</div>

<%= form.date_field :date, label: true %>
<%= form.date_field :date, label: true, value: Date.current %>

<div data-trade-form-target="amountInput" hidden>
<%= form.money_field :amount, label: t(".amount"), disable_currency: true %>
Expand Down
7 changes: 7 additions & 0 deletions app/views/account/trades/_tickers.turbo_stream.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div class="flex items-center">
<%= image_tag("https://logo.synthfinance.com/ticker/#{tickers&.ticker}", class: "rounded-full h-8 w-8 inline-block mr-2") %>
<div class="flex flex-col">
<span class="text-sm font-medium"><%= tickers&.name.presence || tickers&.ticker %></span>
<span class="text-xs text-gray-500"><%= "#{tickers&.ticker} (#{tickers&.exchange_acronym})" %></span>
</div>
</div>
3 changes: 3 additions & 0 deletions app/views/account/trades/securities.turbo_stream.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%= async_combobox_options @securities,
render_in: { partial: "account/trades/tickers" },
next_page: @pagy.next %>
2 changes: 2 additions & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= combobox_style_tag %>
<%= javascript_importmap_tags %>
<%= hotwire_livereload_tags if Rails.env.development? %>
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
Expand Down
4 changes: 3 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@

resources :transactions, only: %i[index update]
resources :valuations, only: %i[index new create]
resources :trades, only: %i[index new create update]
resources :trades, only: %i[index new create update] do
get :securities, on: :collection
end

resources :entries, only: %i[edit update show destroy]
end
Expand Down
6 changes: 6 additions & 0 deletions db/migrate/20241025182612_add_search_vector_to_securities.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddSearchVectorToSecurities < ActiveRecord::Migration[7.2]
def change
add_column :securities, :search_vector, :virtual, type: :tsvector, as: "setweight(to_tsvector('simple', coalesce(ticker, '')), 'B') || to_tsvector('simple', coalesce(name, ''))", stored: true
add_index :securities, :search_vector, using: :gin
end
end
4 changes: 3 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions test/fixtures/securities.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
aapl:
ticker: AAPL
name: Apple
exchange_mic: XNAS

msft:
ticker: MSFT
name: Microsoft
exchange_mic: XNAS
12 changes: 10 additions & 2 deletions test/system/trades_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ class TradesTest < ApplicationSystemTestCase

open_new_trade_modal

fill_in "Ticker symbol", with: "NVDA"
fill_in "Ticker symbol", with: "AAPL"
select_combobox_option("Apple")
fill_in "Date", with: Date.current
fill_in "Quantity", with: shares_qty
fill_in "account_entry[price]", with: 214.23
Expand All @@ -27,7 +28,7 @@ class TradesTest < ApplicationSystemTestCase

within_trades do
assert_text "Purchase 10 shares of AAPL"
assert_text "Buy #{shares_qty} shares of NVDA"
assert_text "Buy #{shares_qty} shares of AAPL"
end
end

Expand All @@ -38,6 +39,7 @@ class TradesTest < ApplicationSystemTestCase

select "Sell", from: "Type"
fill_in "Ticker symbol", with: aapl.ticker
select_combobox_option(aapl.security.name)
fill_in "Date", with: Date.current
fill_in "Quantity", with: aapl.qty
fill_in "account_entry[price]", with: 215.33
Expand All @@ -64,4 +66,10 @@ def within_trades(&block)
def visit_account_trades
visit account_url(@account, tab: "transactions")
end

def select_combobox_option(text)
within "#account_entry_ticker-hw-listbox" do
find("li", text: text).click
end
end
end

0 comments on commit 7d8028b

Please sign in to comment.