From 490f44589e846e5b5d2b6bf56ddb4382234f0067 Mon Sep 17 00:00:00 2001 From: Josh Pigford Date: Tue, 29 Oct 2024 15:37:59 -0400 Subject: [PATCH] First pass at security price reference (#1388) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * First pass at security price reference * Data cleanup * Synth security fetching does better with a mic_code * Update test suite 😭 * Update schema.rb * Update generator.rb --- app/models/account/holding/syncer.rb | 36 +++++++----- app/models/demo/generator.rb | 10 ++-- app/models/provider/synth.rb | 3 +- app/models/security.rb | 3 +- app/models/security/price.rb | 26 ++++----- app/models/security/price/provided.rb | 15 +++-- ...125406_add_reference_to_security_prices.rb | 14 +++++ db/schema.rb | 3 + test/fixtures/security/prices.yml | 4 +- .../security_price_provider_interface_test.rb | 7 ++- test/models/account/holding/syncer_test.rb | 41 +++++++------- test/models/account/holding_test.rb | 2 +- test/models/provider/synth_test.rb | 7 ++- test/models/security/price_test.rb | 56 ++++++++++++------- test/support/account/entries_test_helper.rb | 2 +- test/support/securities_test_helper.rb | 14 ++++- 16 files changed, 155 insertions(+), 88 deletions(-) create mode 100644 db/migrate/20241029125406_add_reference_to_security_prices.rb diff --git a/app/models/account/holding/syncer.rb b/app/models/account/holding/syncer.rb index 9db844f0..f3c7c1fe 100644 --- a/app/models/account/holding/syncer.rb +++ b/app/models/account/holding/syncer.rb @@ -38,23 +38,31 @@ class Account::Holding::Syncer def security_prices @security_prices ||= begin - prices = {} - ticker_start_dates = {} + prices = {} + ticker_securities = {} - sync_entries.each do |entry| - unless ticker_start_dates[entry.account_trade.security.ticker] - ticker_start_dates[entry.account_trade.security.ticker] = entry.date - end - end + sync_entries.each do |entry| + security = entry.account_trade.security + unless ticker_securities[security.ticker] + ticker_securities[security.ticker] = { + security: security, + start_date: entry.date + } + end + end - ticker_start_dates.each do |ticker, date| - fetched_prices = Security::Price.find_prices(ticker: ticker, start_date: date, end_date: Date.current) - gapfilled_prices = Gapfiller.new(fetched_prices, start_date: date, end_date: Date.current, cache: false).run - prices[ticker] = gapfilled_prices - end + ticker_securities.each do |ticker, data| + fetched_prices = Security::Price.find_prices( + security: data[:security], + start_date: data[:start_date], + end_date: Date.current + ) + gapfilled_prices = Gapfiller.new(fetched_prices, start_date: data[:start_date], end_date: Date.current, cache: false).run + prices[ticker] = gapfilled_prices + end - prices - end + prices + end end def build_holdings_for_date(date) diff --git a/app/models/demo/generator.rb b/app/models/demo/generator.rb index ba210b2b..b56ef1bd 100644 --- a/app/models/demo/generator.rb +++ b/app/models/demo/generator.rb @@ -176,12 +176,12 @@ class Demo::Generator def load_securities! # Create an unknown security to simulate edge cases - Security.create! ticker: "UNKNOWN", name: "Unknown Demo Stock" + Security.create! ticker: "UNKNOWN", name: "Unknown Demo Stock", exchange_mic: "UNKNOWN" securities = [ - { ticker: "AAPL", name: "Apple Inc.", reference_price: 210 }, - { ticker: "TM", name: "Toyota Motor Corporation", reference_price: 202 }, - { ticker: "MSFT", name: "Microsoft Corporation", reference_price: 455 } + { ticker: "AAPL", exchange_mic: "NASDAQ", name: "Apple Inc.", reference_price: 210 }, + { ticker: "TM", exchange_mic: "NYSE", name: "Toyota Motor Corporation", reference_price: 202 }, + { ticker: "MSFT", exchange_mic: "NASDAQ", name: "Microsoft Corporation", reference_price: 455 } ] securities.each do |security_attributes| @@ -193,7 +193,7 @@ class Demo::Generator low_price = reference - 20 high_price = reference + 20 Security::Price.create! \ - ticker: security.ticker, + security: security, date: date, price: Faker::Number.positive(from: low_price, to: high_price) end diff --git a/app/models/provider/synth.rb b/app/models/provider/synth.rb index 17d6954e..d5f28e50 100644 --- a/app/models/provider/synth.rb +++ b/app/models/provider/synth.rb @@ -42,9 +42,10 @@ class Provider::Synth ) end - def fetch_security_prices(ticker:, start_date:, end_date:) + def fetch_security_prices(ticker:, mic_code:, start_date:, end_date:) prices = paginate( "#{base_url}/tickers/#{ticker}/open-close", + mic_code: mic_code, start_date: start_date, end_date: end_date ) do |body| diff --git a/app/models/security.rb b/app/models/security.rb index 94077182..d1a160b8 100644 --- a/app/models/security.rb +++ b/app/models/security.rb @@ -2,6 +2,7 @@ class Security < ApplicationRecord before_save :upcase_ticker has_many :trades, dependent: :nullify, class_name: "Account::Trade" + has_many :prices, dependent: :destroy validates :ticker, presence: true validates :ticker, uniqueness: { scope: :exchange_mic, case_sensitive: false } @@ -26,7 +27,7 @@ class Security < ApplicationRecord } def current_price - @current_price ||= Security::Price.find_price(ticker:, date: Date.current) + @current_price ||= Security::Price.find_price(security: self, date: Date.current) return nil if @current_price.nil? Money.new(@current_price.price, @current_price.currency) end diff --git a/app/models/security/price.rb b/app/models/security/price.rb index e1b90fb5..cbec42b3 100644 --- a/app/models/security/price.rb +++ b/app/models/security/price.rb @@ -1,33 +1,31 @@ class Security::Price < ApplicationRecord include Provided - before_save :upcase_ticker - - validates :ticker, presence: true, uniqueness: { scope: :date, case_sensitive: false } + belongs_to :security class << self - def find_price(ticker:, date:, cache: true) - result = find_by(ticker:, date:) + def find_price(security:, date:, cache: true) + result = find_by(security:, date:) - result || fetch_price_from_provider(ticker:, date:, cache:) + result || fetch_price_from_provider(security:, date:, cache:) end - def find_prices(ticker:, start_date:, end_date: Date.current, cache: true) - prices = where(ticker:, date: start_date..end_date).to_a + def find_prices(security:, start_date:, end_date: Date.current, cache: true) + prices = where(security_id: security.id, date: start_date..end_date).to_a all_dates = (start_date..end_date).to_a.to_set existing_dates = prices.map(&:date).to_set missing_dates = (all_dates - existing_dates).sort if missing_dates.any? - prices += fetch_prices_from_provider(ticker:, start_date: missing_dates.first, end_date: missing_dates.last, cache:) + prices += fetch_prices_from_provider( + security: security, + start_date: missing_dates.first, + end_date: missing_dates.last, + cache: cache + ) end prices end end - - private - def upcase_ticker - self.ticker = ticker.upcase - end end diff --git a/app/models/security/price/provided.rb b/app/models/security/price/provided.rb index 599f19ff..02e5ce14 100644 --- a/app/models/security/price/provided.rb +++ b/app/models/security/price/provided.rb @@ -6,17 +6,18 @@ module Security::Price::Provided class_methods do private - def fetch_price_from_provider(ticker:, date:, cache: false) + def fetch_price_from_provider(security:, date:, cache: false) return nil unless security_prices_provider.present? response = security_prices_provider.fetch_security_prices \ - ticker: ticker, + ticker: security.ticker, + mic_code: security.exchange_mic, start_date: date, end_date: date if response.success? && response.prices.size > 0 price = Security::Price.new \ - ticker: ticker, + security: security, date: response.prices.first[:date], price: response.prices.first[:price], currency: response.prices.first[:currency] @@ -28,18 +29,20 @@ module Security::Price::Provided end end - def fetch_prices_from_provider(ticker:, start_date:, end_date:, cache: false) + def fetch_prices_from_provider(security:, start_date:, end_date:, cache: false) return [] unless security_prices_provider.present? + return [] unless security response = security_prices_provider.fetch_security_prices \ - ticker: ticker, + ticker: security.ticker, + mic_code: security.exchange_mic, start_date: start_date, end_date: end_date if response.success? response.prices.map do |price| new_price = Security::Price.find_or_initialize_by( - ticker: ticker, + security: security, date: price[:date] ) do |p| p.price = price[:price] diff --git a/db/migrate/20241029125406_add_reference_to_security_prices.rb b/db/migrate/20241029125406_add_reference_to_security_prices.rb new file mode 100644 index 00000000..87a7ec2c --- /dev/null +++ b/db/migrate/20241029125406_add_reference_to_security_prices.rb @@ -0,0 +1,14 @@ +class AddReferenceToSecurityPrices < ActiveRecord::Migration[7.2] + def change + add_reference :security_prices, :security, foreign_key: true, type: :uuid + + reversible do |dir| + dir.up do + Security::Price.find_each do |sp| + security = Security.find_by(ticker: sp.ticker) + sp.update_column(:security_id, security&.id) + end + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 119c1b1d..507fd0f9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -494,6 +494,8 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_29_184115) do t.string "currency", default: "USD" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.uuid "security_id" + t.index ["security_id"], name: "index_security_prices_on_security_id" end create_table "sessions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -606,6 +608,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_29_184115) do add_foreign_key "imports", "families" add_foreign_key "institutions", "families" add_foreign_key "merchants", "families" + add_foreign_key "security_prices", "securities" add_foreign_key "sessions", "impersonation_sessions", column: "active_impersonator_session_id" add_foreign_key "sessions", "users" add_foreign_key "taggings", "tags" diff --git a/test/fixtures/security/prices.yml b/test/fixtures/security/prices.yml index f49de36c..75686bac 100644 --- a/test/fixtures/security/prices.yml +++ b/test/fixtures/security/prices.yml @@ -1,11 +1,11 @@ one: - ticker: AAPL + security: aapl date: <%= Date.current %> price: 215 currency: USD two: - ticker: AAPL + security: aapl date: <%= 1.day.ago.to_date %> price: 214 currency: USD diff --git a/test/interfaces/security_price_provider_interface_test.rb b/test/interfaces/security_price_provider_interface_test.rb index c5642656..484d25fa 100644 --- a/test/interfaces/security_price_provider_interface_test.rb +++ b/test/interfaces/security_price_provider_interface_test.rb @@ -10,7 +10,12 @@ module SecurityPriceProviderInterfaceTest test "security price provider response contract" do VCR.use_cassette "synth/security_prices" do - response = @subject.fetch_security_prices ticker: "AAPL", start_date: Date.iso8601("2024-01-01"), end_date: Date.iso8601("2024-08-01") + response = @subject.fetch_security_prices( + ticker: "AAPL", + mic_code: "XNAS", + start_date: Date.iso8601("2024-01-01"), + end_date: Date.iso8601("2024-08-01") + ) assert_respond_to response, :prices assert_respond_to response, :success? diff --git a/test/models/account/holding/syncer_test.rb b/test/models/account/holding/syncer_test.rb index a0c63fd6..6d04fb3e 100644 --- a/test/models/account/holding/syncer_test.rb +++ b/test/models/account/holding/syncer_test.rb @@ -14,6 +14,7 @@ class Account::Holding::SyncerTest < ActiveSupport::TestCase end test "can buy and sell securities" do + # First create securities with their prices security1 = create_security("AMZN", prices: [ { date: 2.days.ago.to_date, price: 214 }, { date: 1.day.ago.to_date, price: 215 }, @@ -25,19 +26,18 @@ class Account::Holding::SyncerTest < ActiveSupport::TestCase { date: Date.current, price: 124 } ]) + # Then create trades after prices exist create_trade(security1, account: @account, qty: 10, date: 2.days.ago.to_date) # buy 10 shares of AMZN - create_trade(security1, account: @account, qty: 2, date: 1.day.ago.to_date) # buy 2 shares of AMZN create_trade(security2, account: @account, qty: 20, date: 1.day.ago.to_date) # buy 20 shares of NVDA - create_trade(security1, account: @account, qty: -10, date: Date.current) # sell 10 shares of AMZN expected = [ - { ticker: "AMZN", qty: 10, price: 214, amount: 10 * 214, date: 2.days.ago.to_date }, - { ticker: "AMZN", qty: 12, price: 215, amount: 12 * 215, date: 1.day.ago.to_date }, - { ticker: "AMZN", qty: 2, price: 216, amount: 2 * 216, date: Date.current }, - { ticker: "NVDA", qty: 20, price: 122, amount: 20 * 122, date: 1.day.ago.to_date }, - { ticker: "NVDA", qty: 20, price: 124, amount: 20 * 124, date: Date.current } + { security: security1, qty: 10, price: 214, amount: 10 * 214, date: 2.days.ago.to_date }, + { security: security1, qty: 12, price: 215, amount: 12 * 215, date: 1.day.ago.to_date }, + { security: security1, qty: 2, price: 216, amount: 2 * 216, date: Date.current }, + { security: security2, qty: 20, price: 122, amount: 20 * 122, date: 1.day.ago.to_date }, + { security: security2, qty: 20, price: 124, amount: 20 * 124, date: Date.current } ] run_sync_for(@account) @@ -55,7 +55,7 @@ class Account::Holding::SyncerTest < ActiveSupport::TestCase create_trade(amzn, account: @account, qty: 10, date: Date.current, price: 215) expected = [ - { ticker: "AMZN", qty: 10, price: 215, amount: 10 * 215, date: Date.current } + { security: amzn, qty: 10, price: 215, amount: 10 * 215, date: Date.current } ] run_sync_for(@account) @@ -72,16 +72,16 @@ class Account::Holding::SyncerTest < ActiveSupport::TestCase # 1 day ago — finds daily price, uses it # Today — no daily price, no entry, so price and amount are `nil` expected = [ - { ticker: "AMZN", qty: 10, price: 210, amount: 10 * 210, date: 2.days.ago.to_date }, - { ticker: "AMZN", qty: 10, price: 215, amount: 10 * 215, date: 1.day.ago.to_date }, - { ticker: "AMZN", qty: 10, price: nil, amount: nil, date: Date.current } + { security: amzn, qty: 10, price: 210, amount: 10 * 210, date: 2.days.ago.to_date }, + { security: amzn, qty: 10, price: 215, amount: 10 * 215, date: 1.day.ago.to_date }, + { security: amzn, qty: 10, price: nil, amount: nil, date: Date.current } ] fetched_prices = [ Security::Price.new(ticker: "AMZN", date: 1.day.ago.to_date, price: 215) ] Gapfiller.any_instance.expects(:run).returns(fetched_prices) Security::Price.expects(:find_prices) - .with(start_date: 2.days.ago.to_date, end_date: Date.current, ticker: "AMZN") + .with(security: amzn, start_date: 2.days.ago.to_date, end_date: Date.current) .once .returns(fetched_prices) @@ -103,13 +103,13 @@ class Account::Holding::SyncerTest < ActiveSupport::TestCase { date: monday, price: 220 } ]) - create_trade(tm, account: @account, qty: 10, date: friday) + create_trade(tm, account: @account, qty: 10, date: friday, price: 210) expected = [ - { ticker: "TM", qty: 10, price: 210, amount: 10 * 210, date: friday }, - { ticker: "TM", qty: 10, price: 210, amount: 10 * 210, date: saturday }, - { ticker: "TM", qty: 10, price: 210, amount: 10 * 210, date: sunday }, - { ticker: "TM", qty: 10, price: 220, amount: 10 * 220, date: monday } + { security: tm, qty: 10, price: 210, amount: 10 * 210, date: friday }, + { security: tm, qty: 10, price: 210, amount: 10 * 210, date: saturday }, + { security: tm, qty: 10, price: 210, amount: 10 * 210, date: sunday }, + { security: tm, qty: 10, price: 220, amount: 10 * 220, date: monday } ] run_sync_for(@account) @@ -122,12 +122,15 @@ class Account::Holding::SyncerTest < ActiveSupport::TestCase def assert_holdings(expected_holdings) holdings = @account.holdings.includes(:security).to_a expected_holdings.each do |expected_holding| - actual_holding = holdings.find { |holding| holding.security.ticker == expected_holding[:ticker] && holding.date == expected_holding[:date] } + actual_holding = holdings.find { |holding| + holding.security == expected_holding[:security] && + holding.date == expected_holding[:date] + } date = expected_holding[:date] expected_price = expected_holding[:price] expected_qty = expected_holding[:qty] expected_amount = expected_holding[:amount] - ticker = expected_holding[:ticker] + ticker = expected_holding[:security].ticker assert actual_holding, "expected #{ticker} holding on date: #{date}" assert_equal expected_holding[:qty], actual_holding.qty, "expected #{expected_qty} qty for holding #{ticker} on date: #{date}" diff --git a/test/models/account/holding_test.rb b/test/models/account/holding_test.rb index e7ba1866..5feb0562 100644 --- a/test/models/account/holding_test.rb +++ b/test/models/account/holding_test.rb @@ -58,7 +58,7 @@ class Account::HoldingTest < ActiveSupport::TestCase end def create_holding(security, date, qty) - price = Security::Price.find_by(date: date, ticker: security.ticker).price + price = Security::Price.find_by(date: date, security: security).price @account.holdings.create! \ date: date, diff --git a/test/models/provider/synth_test.rb b/test/models/provider/synth_test.rb index 3d0d5b72..fdca07c3 100644 --- a/test/models/provider/synth_test.rb +++ b/test/models/provider/synth_test.rb @@ -10,7 +10,12 @@ class Provider::SynthTest < ActiveSupport::TestCase test "fetches paginated securities prices" do VCR.use_cassette("synth/security_prices") do - response = @synth.fetch_security_prices ticker: "AAPL", start_date: Date.iso8601("2024-01-01"), end_date: Date.iso8601("2024-08-01") + response = @synth.fetch_security_prices( + ticker: "AAPL", + mic_code: "XNAS", + start_date: Date.iso8601("2024-01-01"), + end_date: Date.iso8601("2024-08-01") + ) assert 213, response.size end diff --git a/test/models/security/price_test.rb b/test/models/security/price_test.rb index b2ae23a1..7ac65ac9 100644 --- a/test/models/security/price_test.rb +++ b/test/models/security/price_test.rb @@ -18,54 +18,63 @@ class Security::PriceTest < ActiveSupport::TestCase test "finds single security price in DB" do @provider.expects(:fetch_security_prices).never + security = securities(:aapl) price = security_prices(:one) - assert_equal price, Security::Price.find_price(ticker: price.ticker, date: price.date) + assert_equal price, Security::Price.find_price(security: security, date: price.date) end test "caches prices to DB" do expected_price = 314.34 - @provider.expects(:fetch_security_prices) - .once - .returns( - OpenStruct.new( - success?: true, - prices: [ { date: Date.current, price: expected_price } ] - ) - ) + security = securities(:aapl) + tomorrow = Date.current + 1.day - fetched_rate = Security::Price.find_price(ticker: "NVDA", date: Date.current, cache: true) - refetched_rate = Security::Price.find_price(ticker: "NVDA", date: Date.current, cache: true) + @provider.expects(:fetch_security_prices) + .with(ticker: security.ticker, mic_code: security.exchange_mic, start_date: tomorrow, end_date: tomorrow) + .once + .returns( + OpenStruct.new( + success?: true, + prices: [ { date: tomorrow, price: expected_price } ] + ) + ) + + fetched_rate = Security::Price.find_price(security: security, date: tomorrow, cache: true) + refetched_rate = Security::Price.find_price(security: security, date: tomorrow, cache: true) assert_equal expected_price, fetched_rate.price assert_equal expected_price, refetched_rate.price end test "returns nil if no price found in DB or from provider" do + security = securities(:aapl) + Security::Price.delete_all # Clear any existing prices + @provider.expects(:fetch_security_prices) - .with(ticker: "NVDA", start_date: Date.current, end_date: Date.current) + .with(ticker: security.ticker, mic_code: security.exchange_mic, start_date: Date.current, end_date: Date.current) .once .returns(OpenStruct.new(success?: false)) - assert_not Security::Price.find_price(ticker: "NVDA", date: Date.current) + assert_not Security::Price.find_price(security: security, date: Date.current) end test "returns nil if price not found in DB and provider disabled" do Security::Price.unstub(:security_prices_provider) + security = Security.new(ticker: "NVDA") with_env_overrides SYNTH_API_KEY: nil do - assert_not Security::Price.find_price(ticker: "NVDA", date: Date.current) + assert_not Security::Price.find_price(security: security, date: Date.current) end end test "fetches multiple dates at once" do @provider.expects(:fetch_security_prices).never - + security = securities(:aapl) price1 = security_prices(:one) # AAPL today price2 = security_prices(:two) # AAPL yesterday - fetched_prices = Security::Price.find_prices(start_date: 1.day.ago.to_date, end_date: Date.current, ticker: "AAPL").sort_by(&:date) + fetched_prices = Security::Price.find_prices(security: security, start_date: 1.day.ago.to_date, end_date: Date.current).sort_by(&:date) assert_equal price1, fetched_prices[1] assert_equal price2, fetched_prices[0] @@ -73,26 +82,33 @@ class Security::PriceTest < ActiveSupport::TestCase test "caches multiple prices to DB" do missing_price = 213.21 + security = securities(:aapl) + @provider.expects(:fetch_security_prices) - .with(ticker: "AAPL", start_date: 2.days.ago.to_date, end_date: 2.days.ago.to_date) + .with(ticker: security.ticker, + mic_code: security.exchange_mic, + start_date: 2.days.ago.to_date, + end_date: 2.days.ago.to_date) .returns(OpenStruct.new(success?: true, prices: [ { date: 2.days.ago.to_date, price: missing_price } ])) .once price1 = security_prices(:one) # AAPL today price2 = security_prices(:two) # AAPL yesterday - fetched_prices = Security::Price.find_prices(ticker: "AAPL", start_date: 2.days.ago.to_date, end_date: Date.current, cache: true) - refetched_prices = Security::Price.find_prices(ticker: "AAPL", start_date: 2.days.ago.to_date, end_date: Date.current, cache: true) + fetched_prices = Security::Price.find_prices(security: security, start_date: 2.days.ago.to_date, end_date: Date.current, cache: true) + refetched_prices = Security::Price.find_prices(security: security, start_date: 2.days.ago.to_date, end_date: Date.current, cache: true) assert_equal [ missing_price, price2.price, price1.price ], fetched_prices.sort_by(&:date).map(&:price) assert_equal [ missing_price, price2.price, price1.price ], refetched_prices.sort_by(&:date).map(&:price) + + assert Security::Price.exists?(security: security, date: 2.days.ago.to_date, price: missing_price) end test "returns empty array if no prices found in DB or from provider" do Security::Price.unstub(:security_prices_provider) with_env_overrides SYNTH_API_KEY: nil do - assert_equal [], Security::Price.find_prices(ticker: "NVDA", start_date: 10.days.ago.to_date, end_date: Date.current) + assert_equal [], Security::Price.find_prices(security: Security.new(ticker: "NVDA"), start_date: 10.days.ago.to_date, end_date: Date.current) end end end diff --git a/test/support/account/entries_test_helper.rb b/test/support/account/entries_test_helper.rb index fb0356f4..d6152381 100644 --- a/test/support/account/entries_test_helper.rb +++ b/test/support/account/entries_test_helper.rb @@ -29,7 +29,7 @@ module Account::EntriesTestHelper end def create_trade(security, account:, qty:, date:, price: nil) - trade_price = price || Security::Price.find_by!(ticker: security.ticker, date: date).price + trade_price = price || Security::Price.find_by!(security: security, date: date).price trade = Account::Trade.new \ qty: qty, diff --git a/test/support/securities_test_helper.rb b/test/support/securities_test_helper.rb index b1ca5afc..2da334a6 100644 --- a/test/support/securities_test_helper.rb +++ b/test/support/securities_test_helper.rb @@ -1,9 +1,19 @@ module SecuritiesTestHelper def create_security(ticker, prices:) + security = Security.create!( + ticker: ticker, + exchange_mic: "XNAS" + ) + prices.each do |price| - Security::Price.create! ticker: ticker, date: price[:date], price: price[:price] + Security::Price.create!( + security: security, + date: price[:date], + price: price[:price], + currency: "USD" + ) end - Security.create! ticker: ticker + security end end