Initial pass at Synth-based ticker selection #1392

Merged
Shpigford merged 15 commits from synth-selector into main 2024-10-30 21:23:45 +08:00
Shpigford commented 2024-10-30 06:13:47 +08:00 (Migrated from github.com)
No description provided.
Shpigford commented 2024-10-30 07:09:50 +08:00 (Migrated from github.com)

@josefarias Sorry to keep pinging you about this but alas...

https://github.com/user-attachments/assets/cf2821e6-2c54-4954-aecf-c8f7d1beb7eb

Selecting the item from the dropdown is filling in the input with the full hash. Not sure how to address that.

What I'm ultimately trying to do is have the fancy dropdown and then have the value on selection be something like AAPL (NYSE) (ticker + mic code).

I'm almost certainly doing something very wrong here...I just don't know what...

<%= async_combobox_options @securities.map { |security| 
      { 
        display: security,
        value: security[:symbol]
      }
    },
    render_in: { partial: "account/trades/tickers" } %>

@securities is an array of hashes...

[{:symbol=>"AAPL", :name=>"Apple Inc.", :logo_url=>"https://logo.synthfinance.com/ticker/AAPL", :exchange_acronym=>"NASDAQ"}, {:symbol=>"AAPL", :name=>"Apple Inc", :logo_url=>"https://logo.synthfinance.com/ticker/AAPL", :exchange_acronym=>"NASDAQ"}, {:symbol=>"AAPD", :name=>"Direxion Daily AAPL Bear 1X Shares", :logo_url=>"https://logo.synthfinance.com/ticker/AAPD", :exchange_acronym=>"NASDAQ"}, {:symbol=>"AAPY", :name=>"Kurv Yield Premium Strategy Apple (AAPL) ETF", :logo_url=>"https://logo.synthfinance.com/ticker/AAPY", :exchange_acronym=>"CBOE"}, {:symbol=>"APLY", :name=>"YieldMax AAPL Option Income Strategy ETF", :logo_url=>"https://logo.synthfinance.com/ticker/APLY", :exchange_acronym=>"NYSE"}, {:symbol=>"AAPU", :name=>"Direxion Shares ETF Trust Direxion Daily AAPL Bull 2X Shares", :logo_url=>"https://logo.synthfinance.com/ticker/AAPU", :exchange_acronym=>"NASDAQ"}, {:symbol=>"AAPB", :name=>"GraniteShares ETF Trust GraniteShares 2x Long AAPL Daily ETF", :logo_url=>"https://logo.synthfinance.com/ticker/AAPB", :exchange_acronym=>"NASDAQ"}]
@josefarias Sorry to keep pinging you about this but alas... https://github.com/user-attachments/assets/cf2821e6-2c54-4954-aecf-c8f7d1beb7eb Selecting the item from the dropdown is filling in the input with the full hash. Not sure how to address that. What I'm _ultimately_ trying to do is have the fancy dropdown and then have the value on selection be something like `AAPL (NYSE)` (ticker + mic code). I'm almost certainly doing something very wrong here...I just don't know what... ```ruby <%= async_combobox_options @securities.map { |security| { display: security, value: security[:symbol] } }, render_in: { partial: "account/trades/tickers" } %> ``` `@securities` is an array of hashes... ``` [{:symbol=>"AAPL", :name=>"Apple Inc.", :logo_url=>"https://logo.synthfinance.com/ticker/AAPL", :exchange_acronym=>"NASDAQ"}, {:symbol=>"AAPL", :name=>"Apple Inc", :logo_url=>"https://logo.synthfinance.com/ticker/AAPL", :exchange_acronym=>"NASDAQ"}, {:symbol=>"AAPD", :name=>"Direxion Daily AAPL Bear 1X Shares", :logo_url=>"https://logo.synthfinance.com/ticker/AAPD", :exchange_acronym=>"NASDAQ"}, {:symbol=>"AAPY", :name=>"Kurv Yield Premium Strategy Apple (AAPL) ETF", :logo_url=>"https://logo.synthfinance.com/ticker/AAPY", :exchange_acronym=>"CBOE"}, {:symbol=>"APLY", :name=>"YieldMax AAPL Option Income Strategy ETF", :logo_url=>"https://logo.synthfinance.com/ticker/APLY", :exchange_acronym=>"NYSE"}, {:symbol=>"AAPU", :name=>"Direxion Shares ETF Trust Direxion Daily AAPL Bull 2X Shares", :logo_url=>"https://logo.synthfinance.com/ticker/AAPU", :exchange_acronym=>"NASDAQ"}, {:symbol=>"AAPB", :name=>"GraniteShares ETF Trust GraniteShares 2x Long AAPL Daily ETF", :logo_url=>"https://logo.synthfinance.com/ticker/AAPB", :exchange_acronym=>"NASDAQ"}] ```
Shpigford commented 2024-10-30 07:55:10 +08:00 (Migrated from github.com)

@zachgoll If you could review how I'm using the Providable stuff, that'd be great. It seems to work, but just not positive I'm using it as intended.

@zachgoll If you could review how I'm using the Providable stuff, that'd be great. It seems to work, but just not positive I'm using it as intended.
zachgoll (Migrated from github.com) reviewed 2024-10-30 08:40:17 +08:00
@@ -34,7 +34,10 @@ class Account::TradesController < ApplicationController
end
zachgoll (Migrated from github.com) commented 2024-10-30 08:40:17 +08:00

The only change with the provider here is we'll need to set the key via Setting.synth_api_key (as we're doing in Providable) for self hosting to work.

The diff below should make that work and the combobox display work (note: I renamed _tickers.turbo_stream.erb to _security.turbo_stream.erb)

diff --git a/app/controllers/account/trades_controller.rb b/app/controllers/account/trades_controller.rb
index 8d31d3a..04f2048 100644
--- a/app/controllers/account/trades_controller.rb
+++ b/app/controllers/account/trades_controller.rb
@@ -37,8 +37,17 @@ class Account::TradesController < ApplicationController
     query = params[:q]
     return render json: [] if query.blank? || query.length < 2 || query.length > 100
 
-    synth_client = Provider::Synth.new(ENV["SYNTH_API_KEY"])
-    @securities = synth_client.search_securities(query:, dataset: "limited", country_code: Current.family.country).securities
+    @securities = Security.security_prices_provider.search_securities(query:, dataset: "limited", country_code: Current.family.country).securities.map do |security|
+      new_security = Security.new(
+        ticker: security[:symbol],
+        name: security[:name],
+        exchange_acronym: security[:exchange_acronym]
+      )
+
+      new_security.define_singleton_method(:logo_url) { security[:logo_url] }
+
+      new_security
+    end
   end
 
   private
diff --git a/app/models/security.rb b/app/models/security.rb
index 7192239..4558b85 100644
--- a/app/models/security.rb
+++ b/app/models/security.rb
@@ -1,4 +1,6 @@
 class Security < ApplicationRecord
+  include Providable
+
   before_save :upcase_ticker
 
   has_many :trades, dependent: :nullify, class_name: "Account::Trade"
@@ -17,7 +19,6 @@ class Security < ApplicationRecord
     "#{ticker} - #{name} (#{exchange_acronym})"
   end
 
-
   private
 
     def upcase_ticker
diff --git a/app/views/account/trades/_tickers.turbo_stream.erb b/app/views/account/trades/_tickers.turbo_stream.erb
deleted file mode 100644
-</div>
\ No newline at end of file
diff --git a/app/views/account/trades/securities.turbo_stream.erb b/app/views/account/trades/securities.turbo_stream.erb
index f7fcaf2..a3128c7 100644
--- a/app/views/account/trades/securities.turbo_stream.erb
+++ b/app/views/account/trades/securities.turbo_stream.erb
@@ -1,7 +1,2 @@
-<%= async_combobox_options @securities.map { |security| 
-      { 
-        display: security,
-        value: security[:symbol]
-      }
-    },
-    render_in: { partial: "account/trades/tickers" } %>
\ No newline at end of file
+<%= async_combobox_options @securities,
+    render_in: { partial: "account/trades/security" } %>
\ No newline at end of file
The only change with the provider here is we'll need to set the key via `Setting.synth_api_key` (as we're doing in `Providable`) for self hosting to work. The diff below should make that work _and_ the combobox display work (**note:** I renamed `_tickers.turbo_stream.erb` to `_security.turbo_stream.erb`) ```diff diff --git a/app/controllers/account/trades_controller.rb b/app/controllers/account/trades_controller.rb index 8d31d3a..04f2048 100644 --- a/app/controllers/account/trades_controller.rb +++ b/app/controllers/account/trades_controller.rb @@ -37,8 +37,17 @@ class Account::TradesController < ApplicationController query = params[:q] return render json: [] if query.blank? || query.length < 2 || query.length > 100 - synth_client = Provider::Synth.new(ENV["SYNTH_API_KEY"]) - @securities = synth_client.search_securities(query:, dataset: "limited", country_code: Current.family.country).securities + @securities = Security.security_prices_provider.search_securities(query:, dataset: "limited", country_code: Current.family.country).securities.map do |security| + new_security = Security.new( + ticker: security[:symbol], + name: security[:name], + exchange_acronym: security[:exchange_acronym] + ) + + new_security.define_singleton_method(:logo_url) { security[:logo_url] } + + new_security + end end private diff --git a/app/models/security.rb b/app/models/security.rb index 7192239..4558b85 100644 --- a/app/models/security.rb +++ b/app/models/security.rb @@ -1,4 +1,6 @@ class Security < ApplicationRecord + include Providable + before_save :upcase_ticker has_many :trades, dependent: :nullify, class_name: "Account::Trade" @@ -17,7 +19,6 @@ class Security < ApplicationRecord "#{ticker} - #{name} (#{exchange_acronym})" end - private def upcase_ticker diff --git a/app/views/account/trades/_tickers.turbo_stream.erb b/app/views/account/trades/_tickers.turbo_stream.erb deleted file mode 100644 -</div> \ No newline at end of file diff --git a/app/views/account/trades/securities.turbo_stream.erb b/app/views/account/trades/securities.turbo_stream.erb index f7fcaf2..a3128c7 100644 --- a/app/views/account/trades/securities.turbo_stream.erb +++ b/app/views/account/trades/securities.turbo_stream.erb @@ -1,7 +1,2 @@ -<%= async_combobox_options @securities.map { |security| - { - display: security, - value: security[:symbol] - } - }, - render_in: { partial: "account/trades/tickers" } %> \ No newline at end of file +<%= async_combobox_options @securities, + render_in: { partial: "account/trades/security" } %> \ No newline at end of file ```
zachgoll (Migrated from github.com) reviewed 2024-10-30 08:42:08 +08:00
@@ -34,7 +34,10 @@ class Account::TradesController < ApplicationController
end
zachgoll (Migrated from github.com) commented 2024-10-30 08:42:08 +08:00

And then this is the new _security.turbo_stream.erb:

<div class="flex items-center">
  <%= image_tag(security.logo_url, class: "rounded-full h-8 w-8 inline-block mr-2" ) %>
  <div class="flex flex-col">
    <span class="text-sm font-medium">
      <%= security.name.presence || security.ticker %>
    </span>
    <span class="text-xs text-gray-500">
      <%= "#{security.ticker} (#{security.exchange_acronym})" %>
    </span>
  </div>
</div>
And then this is the new `_security.turbo_stream.erb`: ```erb <div class="flex items-center"> <%= image_tag(security.logo_url, class: "rounded-full h-8 w-8 inline-block mr-2" ) %> <div class="flex flex-col"> <span class="text-sm font-medium"> <%= security.name.presence || security.ticker %> </span> <span class="text-xs text-gray-500"> <%= "#{security.ticker} (#{security.exchange_acronym})" %> </span> </div> </div> ```
Shpigford commented 2024-10-30 09:09:30 +08:00 (Migrated from github.com)

Current status: combobox seems functional, however on form submission the "ticker" field is submitting "undefined".

Parameters: {"authenticity_token"=>"[FILTERED]", "account_entry"=>{"type"=>"buy", "ticker"=>"undefined", "date"=>"2024-10-16", "amount"=>"", "transfer_account_id"=>"", "qty"=>"25", "price"=>"100"}, "commit"=>"Add transaction", "account_id"=>"4ab6e059-8bca-490a-9958-8e3fc96346f4"}
Current status: combobox seems functional, however on form submission the "ticker" field is submitting "undefined". ``` Parameters: {"authenticity_token"=>"[FILTERED]", "account_entry"=>{"type"=>"buy", "ticker"=>"undefined", "date"=>"2024-10-16", "amount"=>"", "transfer_account_id"=>"", "qty"=>"25", "price"=>"100"}, "commit"=>"Add transaction", "account_id"=>"4ab6e059-8bca-490a-9958-8e3fc96346f4"} ```
josefarias commented 2024-10-30 09:15:16 +08:00 (Migrated from github.com)

@Shpigford ah, the state of the PR changed after I checked out the code. But the same ideas apply.

This is pretty custom usage, where you essentially have two displays — one for each option in the listbox (HTML) and another to display in the input when an option is selected ("#{symbol} - #{name} (#{exchange_acronym})"). This is possible with HotwireCombobox but it'll require passing in an array of objects. This is working for me locally:

diff --git a/app/controllers/account/trades_controller.rb b/app/controllers/account/trades_controller.rb
index 8d31d3a..ea70af0 100644
--- a/app/controllers/account/trades_controller.rb
+++ b/app/controllers/account/trades_controller.rb
@@ -36,9 +36,7 @@ class Account::TradesController < ApplicationController
   def securities
     query = params[:q]
     return render json: [] if query.blank? || query.length < 2 || query.length > 100
-
-    synth_client = Provider::Synth.new(ENV["SYNTH_API_KEY"])
-    @securities = synth_client.search_securities(query:, dataset: "limited", country_code: Current.family.country).securities
+    @securities = Security::SynthComboboxOption.find_in_synth(query)
   end
 
   private
diff --git a/app/models/security/synth_combobox_option.rb b/app/models/security/synth_combobox_option.rb
new file mode 100644
index 0000000..fe1fe07
--- /dev/null
+++ b/app/models/security/synth_combobox_option.rb
@@ -0,0 +1,22 @@
+class Security::SynthComboboxOption
+  include ActiveModel::Model
+
+  attr_accessor :symbol, :name, :logo_url, :exchange_acronym
+
+  class << self
+    def find_in_synth(query)
+      Provider::Synth.new(ENV["SYNTH_API_KEY"])
+        .search_securities(query:, dataset: "limited", country_code: Current.family.country)
+        .securities
+        .map { |attrs| new(**attrs) }
+    end
+  end
+
+  def id
+    symbol # submitted by combobox as value
+  end
+
+  def to_combobox_display
+    "#{symbol} - #{name} (#{exchange_acronym})" # shown in combobox input when selected
+  end
+end
diff --git a/app/views/account/trades/_tickers.turbo_stream.erb b/app/views/account/trades/_tickers.turbo_stream.erb
index 20bf8f9..9dba791 100644
--- a/app/views/account/trades/_tickers.turbo_stream.erb
+++ b/app/views/account/trades/_tickers.turbo_stream.erb
@@ -1,11 +1,11 @@
 <div class="flex items-center">
-  <%= image_tag(tickers[:logo_url], class: "rounded-full h-8 w-8 inline-block mr-2" ) %>
+  <%= image_tag(tickers.logo_url, 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[:symbol] %>
+        <%= tickers.name.presence || tickers.symbol %>
       </span>
       <span class="text-xs text-gray-500">
-        <%= "#{tickers[:symbol]} (#{tickers[:exchange_acronym]})" %>
+        <%= "#{tickers.symbol} (#{tickers.exchange_acronym})" %>
       </span>
     </div>
-</div>
\ No newline at end of file
+</div>
diff --git a/app/views/account/trades/securities.turbo_stream.erb b/app/views/account/trades/securities.turbo_stream.erb
index f7fcaf2..4e4b1af 100644
--- a/app/views/account/trades/securities.turbo_stream.erb
+++ b/app/views/account/trades/securities.turbo_stream.erb
@@ -1,7 +1 @@
-<%= async_combobox_options @securities.map { |security| 
-      { 
-        display: security,
-        value: security[:symbol]
-      }
-    },
-    render_in: { partial: "account/trades/tickers" } %>
\ No newline at end of file
+<%= async_combobox_options @securities, render_in: { partial: "account/trades/tickers" } %>
@Shpigford ah, the state of the PR changed after I checked out the code. But the same ideas apply. This is pretty custom usage, where you essentially have two displays — one for each option in the listbox (HTML) and another to display in the input when an option is selected (`"#{symbol} - #{name} (#{exchange_acronym})"`). This is possible with HotwireCombobox but it'll require passing in an array of objects. This is working for me locally: ```diff diff --git a/app/controllers/account/trades_controller.rb b/app/controllers/account/trades_controller.rb index 8d31d3a..ea70af0 100644 --- a/app/controllers/account/trades_controller.rb +++ b/app/controllers/account/trades_controller.rb @@ -36,9 +36,7 @@ class Account::TradesController < ApplicationController def securities query = params[:q] return render json: [] if query.blank? || query.length < 2 || query.length > 100 - - synth_client = Provider::Synth.new(ENV["SYNTH_API_KEY"]) - @securities = synth_client.search_securities(query:, dataset: "limited", country_code: Current.family.country).securities + @securities = Security::SynthComboboxOption.find_in_synth(query) end private diff --git a/app/models/security/synth_combobox_option.rb b/app/models/security/synth_combobox_option.rb new file mode 100644 index 0000000..fe1fe07 --- /dev/null +++ b/app/models/security/synth_combobox_option.rb @@ -0,0 +1,22 @@ +class Security::SynthComboboxOption + include ActiveModel::Model + + attr_accessor :symbol, :name, :logo_url, :exchange_acronym + + class << self + def find_in_synth(query) + Provider::Synth.new(ENV["SYNTH_API_KEY"]) + .search_securities(query:, dataset: "limited", country_code: Current.family.country) + .securities + .map { |attrs| new(**attrs) } + end + end + + def id + symbol # submitted by combobox as value + end + + def to_combobox_display + "#{symbol} - #{name} (#{exchange_acronym})" # shown in combobox input when selected + end +end diff --git a/app/views/account/trades/_tickers.turbo_stream.erb b/app/views/account/trades/_tickers.turbo_stream.erb index 20bf8f9..9dba791 100644 --- a/app/views/account/trades/_tickers.turbo_stream.erb +++ b/app/views/account/trades/_tickers.turbo_stream.erb @@ -1,11 +1,11 @@ <div class="flex items-center"> - <%= image_tag(tickers[:logo_url], class: "rounded-full h-8 w-8 inline-block mr-2" ) %> + <%= image_tag(tickers.logo_url, 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[:symbol] %> + <%= tickers.name.presence || tickers.symbol %> </span> <span class="text-xs text-gray-500"> - <%= "#{tickers[:symbol]} (#{tickers[:exchange_acronym]})" %> + <%= "#{tickers.symbol} (#{tickers.exchange_acronym})" %> </span> </div> -</div> \ No newline at end of file +</div> diff --git a/app/views/account/trades/securities.turbo_stream.erb b/app/views/account/trades/securities.turbo_stream.erb index f7fcaf2..4e4b1af 100644 --- a/app/views/account/trades/securities.turbo_stream.erb +++ b/app/views/account/trades/securities.turbo_stream.erb @@ -1,7 +1 @@ -<%= async_combobox_options @securities.map { |security| - { - display: security, - value: security[:symbol] - } - }, - render_in: { partial: "account/trades/tickers" } %> \ No newline at end of file +<%= async_combobox_options @securities, render_in: { partial: "account/trades/tickers" } %> ```
josefarias commented 2024-10-30 09:17:29 +08:00 (Migrated from github.com)

Additionally, I'd suggest renaming app/views/account/trades/_tickers.turbo_stream.erb to app/views/account/trades/_ticker.turbo_stream.erb to avoid further confusion, since the partial is rendered once for each ticker. If you do rename, you'll have to change all the references in the partial from tickers to ticker.

Additionally, I'd suggest renaming `app/views/account/trades/_tickers.turbo_stream.erb` to `app/views/account/trades/_ticker.turbo_stream.erb` to avoid further confusion, since the partial is rendered once for each ticker. If you do rename, you'll have to change all the references in the partial from `tickers` to `ticker`.
Shpigford commented 2024-10-30 09:18:39 +08:00 (Migrated from github.com)

@josefarias Looks like @zachgoll's changes were on the right track (they're pushed to the PR now).

@josefarias Looks like @zachgoll's changes were on the right track (they're pushed to the PR now).
Shpigford commented 2024-10-30 09:24:04 +08:00 (Migrated from github.com)

@josefarias Just merged yours and @zachgoll's solutions and it works beautifully. 🙂 Thanks a ton!

@josefarias Just merged yours and @zachgoll's solutions and it works beautifully. 🙂 Thanks a ton!
zachgoll (Migrated from github.com) reviewed 2024-10-30 20:31:19 +08:00
@@ -0,0 +1,20 @@
class Security::SynthComboboxOption
zachgoll (Migrated from github.com) commented 2024-10-30 20:25:57 +08:00

I would include Providable in this class and access Synth as security_prices_provider here so this will work for self hosters who have Synth setup:

security_prices_provider.search_securities( ... )
I would `include Providable` in this class and access Synth as `security_prices_provider` here so this will work for self hosters who have Synth setup: ``` security_prices_provider.search_securities( ... ) ```
zachgoll (Migrated from github.com) commented 2024-10-30 20:31:15 +08:00

Could we use something more like "#{symbol}|#{exchange_mic}" here, and then in the controller, we don't need all the regular expressions? Then we'd just be able to do:

symbol, exchange_mic = ticker.split("|")

Rather than:

exchange_acronym = ticker.match(/\((.*?)\)/)&.captures&.first
ticker_symbol = ticker.gsub(/\s*\(.*?\)\s*/, "")

I'm assuming no symbols/exchanges has the | character in them... But maybe there is a better delimiter to use here

Could we use something more like `"#{symbol}|#{exchange_mic}"` here, and then in the controller, we don't need all the regular expressions? Then we'd just be able to do: ``` symbol, exchange_mic = ticker.split("|") ``` Rather than: ```rb exchange_acronym = ticker.match(/\((.*?)\)/)&.captures&.first ticker_symbol = ticker.gsub(/\s*\(.*?\)\s*/, "") ``` I'm assuming no symbols/exchanges has the `|` character in them... But maybe there is a better delimiter to use here
Sign in to join this conversation.