Skip to content

Commit

Permalink
Stocks overhaul
Browse files Browse the repository at this point in the history
  • Loading branch information
Shpigford committed Nov 12, 2024
1 parent 849a51c commit 3590458
Show file tree
Hide file tree
Showing 20 changed files with 397 additions and 83 deletions.
14 changes: 11 additions & 3 deletions app/controllers/stocks/chart_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@ class Stocks::ChartController < ApplicationController
# @param time_range [String] The time range for the chart (e.g., "1M", "3M", "6M", "1Y", "5Y")
# @return [JSON] A JSON object containing chart data or an error message
def show
@stock = Stock.find_by(symbol: params[:stock_ticker])
if params[:stock_ticker].include?(":")
symbol, mic_code = params[:stock_ticker].split(":")
@stock = Stock.find_by(symbol:, mic_code:)
else
@stock = Stock.find_by(symbol: params[:stock_ticker], country_code: "US")
end

# return redirect_to stocks_path if @stock.nil?

time_range = params[:time_range].presence || "1M" # Set default if nil or empty

@stock_chart = Rails.cache.fetch("stock_chart/v1/#{@stock.symbol}/#{time_range}", expires_in: 12.hours) do
@stock_chart = Rails.cache.fetch("stock_chart/v1/#{@stock.symbol}:#{@stock.mic_code}/#{time_range}", expires_in: 12.hours) do
headers = {
"Content-Type" => "application/json",
"Authorization" => "Bearer #{ENV['SYNTH_API_KEY']}",
Expand All @@ -25,7 +33,7 @@ def show
start_date, interval = calculate_start_date_and_interval(time_range)

response = Faraday.get(
"https://api.synthfinance.com/tickers/#{@stock.symbol}/open-close",
"https://api.synthfinance.com/tickers/#{@stock.symbol}/open-close?mic_code=#{@stock.mic_code}",
{
start_date: start_date.iso8601,
end_date: end_date.iso8601,
Expand Down
11 changes: 8 additions & 3 deletions app/controllers/stocks/info_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@ class Stocks::InfoController < ApplicationController
# @param stock_ticker [String] The ticker symbol of the stock (passed in the URL)
# @return [Object] Sets @stock and @stock_info instance variables for use in the view
def show
@stock = Stock.find_by(symbol: params[:stock_ticker])
if params[:stock_ticker].include?(":")
symbol, mic_code = params[:stock_ticker].split(":")
@stock = Stock.find_by(symbol:, mic_code:)
else
@stock = Stock.find_by(symbol: params[:stock_ticker], country_code: "US")
end

@stock_info = Rails.cache.fetch("stock_info/v1/#{@stock.symbol}", expires_in: 24.hours) do
@stock_info = Rails.cache.fetch("stock_info/v1/#{@stock.symbol}:#{@stock.mic_code}", expires_in: 24.hours) do
headers = {
"Content-Type" => "application/json",
"Authorization" => "Bearer #{ENV['SYNTH_API_KEY']}",
"X-Source" => "maybe_marketing",
"X-Source-Type" => "api"
}

response = Faraday.get("https://api.synthfinance.com/tickers/#{@stock.symbol}", nil, headers)
response = Faraday.get("https://api.synthfinance.com/tickers/#{@stock.symbol}?mic_code=#{@stock.mic_code}", nil, headers)
JSON.parse(response.body)["data"]
end
end
Expand Down
11 changes: 8 additions & 3 deletions app/controllers/stocks/price_performance_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ class Stocks::PricePerformanceController < ApplicationController
# @param timeframe [String] The timeframe for historical data (default: "24h")
# @return [JSON] Price performance data including low, high, and current prices
def show
@stock = Stock.find_by(symbol: params[:stock_ticker])
if params[:stock_ticker].include?(":")
symbol, mic_code = params[:stock_ticker].split(":")
@stock = Stock.find_by(symbol:, mic_code:)
else
@stock = Stock.find_by(symbol: params[:stock_ticker], country_code: "US")
end
timeframe = params[:timeframe] || "24h"

@price_performance = Rails.cache.fetch("price_performance/v1/#{@stock.symbol}/#{timeframe}", expires_in: 12.hours) do
@price_performance = Rails.cache.fetch("price_performance/v1/#{@stock.symbol}:#{@stock.mic_code}/#{timeframe}", expires_in: 12.hours) do
headers = {
"Content-Type" => "application/json",
"Authorization" => "Bearer #{ENV['SYNTH_API_KEY']}",
Expand Down Expand Up @@ -61,7 +66,7 @@ def show
# @param headers [Hash] HTTP headers for the API request
# @return [Hash, nil] Real-time stock data or nil if the request fails
def fetch_real_time_data(symbol, headers)
response = Faraday.get("https://api.synthfinance.com/tickers/#{symbol}/real-time", nil, headers)
response = Faraday.get("https://api.synthfinance.com/tickers/#{symbol}/real-time?mic_code=#{@stock.mic_code}", nil, headers)
return nil unless response.success?
JSON.parse(response.body)["data"]
rescue Faraday::Error, JSON::ParserError => e
Expand Down
9 changes: 7 additions & 2 deletions app/controllers/stocks/similar_stocks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ class Stocks::SimilarStocksController < ApplicationController
# @param stock_ticker [String] The ticker symbol of the stock to find similar stocks for.
# @return [void]
def show
@stock = Stock.find_by(symbol: params[:stock_ticker])
if params[:stock_ticker].include?(":")
symbol, mic_code = params[:stock_ticker].split(":")
@stock = Stock.find_by(symbol:, mic_code:)
else
@stock = Stock.find_by(symbol: params[:stock_ticker], country_code: "US")
end

headers = {
"Content-Type" => "application/json",
Expand All @@ -16,7 +21,7 @@ def show
"X-Source-Type" => "api"
}

response = Faraday.get("https://api.synthfinance.com/tickers/#{@stock.symbol}/related", nil, headers)
response = Faraday.get("https://api.synthfinance.com/tickers/#{@stock.symbol}/related?mic_code=#{@stock.mic_code}", nil, headers)
data = JSON.parse(response.body)["data"]

@similar_stocks_data = []
Expand Down
11 changes: 8 additions & 3 deletions app/controllers/stocks/statistics_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@ class Stocks::StatisticsController < ApplicationController
# @param stock_ticker [String] The ticker symbol of the stock (passed in the URL)
# @return [void]
def show
@stock = Stock.find_by(symbol: params[:stock_ticker])
if params[:stock_ticker].include?(":")
symbol, mic_code = params[:stock_ticker].split(":")
@stock = Stock.find_by(symbol:, mic_code:)
else
@stock = Stock.find_by(symbol: params[:stock_ticker], country_code: "US")
end

@stock_statistics = Rails.cache.fetch("stock_statistics/v1/#{@stock.symbol}", expires_in: 24.hours) do
@stock_statistics = Rails.cache.fetch("stock_statistics/v1/#{@stock.symbol}:#{@stock.mic_code}", expires_in: 24.hours) do
headers = {
"Content-Type" => "application/json",
"Authorization" => "Bearer #{ENV['SYNTH_API_KEY']}",
"X-Source" => "maybe_marketing",
"X-Source-Type" => "api"
}

response = Faraday.get("https://api.synthfinance.com/tickers/#{@stock.symbol}", nil, headers)
response = Faraday.get("https://api.synthfinance.com/tickers/#{@stock.symbol}?mic_code=#{@stock.mic_code}", nil, headers)
parsed_data = JSON.parse(response.body)["data"]
parsed_data && parsed_data["market_data"] ? parsed_data["market_data"] : nil
end
Expand Down
92 changes: 87 additions & 5 deletions app/controllers/stocks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,37 @@ class StocksController < ApplicationController
# GET /stocks
# GET /stocks?q=AAPL
def index
@exchanges = Stock.where(kind: "stock").where.not(mic_code: nil).distinct.pluck(:exchange, :country_code).compact.sort_by(&:first)
@industries = Stock.where(kind: "stock").where.not(mic_code: nil).where.not(industry: nil).distinct.pluck(:industry, :country_code).compact.sort_by(&:first)
@sectors = Stock.where(kind: "stock").where.not(mic_code: nil).where.not(sector: nil).distinct.pluck(:sector).compact.sort
@total_stocks = Stock.where(kind: "stock").where.not(mic_code: nil).count

if params[:combobox].present?
scope = Stock.order(:name).search(params[:q])
@pagy, @stocks = pagy(scope, limit: 27, size: [ 1, 3, 3, 1 ])
@total_stocks = @pagy.count
render :index, variants: [ :combobox ]
elsif params[:q].present?
redirect_to all_stocks_path(q: params[:q])
end
end

def all
@query = params[:q]
scope = Stock.order(:name).search(@query)
@pagy, @stocks = pagy(scope, limit: 27, size: [ 1, 3, 3, 1 ])
@total_stocks = @pagy.count
scope = Stock.order(:name).where(kind: "stock").where.not(mic_code: nil)

scope = scope.where(exchange: params[:exchange]) if params[:exchange].present?
scope = scope.where(industry: params[:industry]) if params[:industry].present?
scope = scope.where(sector: params[:sector]) if params[:sector].present?

render :index, variants: [ :combobox ] if params[:combobox].present?
if @query.present?
@total_stocks = scope.search(@query).count("DISTINCT stocks.id")
scope = scope.search(@query)
else
@total_stocks = scope.count
end

@pagy, @stocks = pagy(scope, limit: 27, size: [ 1, 3, 3, 1 ])
end

# GET /stocks/:ticker
Expand All @@ -28,6 +53,63 @@ def index
# @example
# GET /stocks/AAPL
def show
@stock = Stock.find_by(symbol: params[:ticker])
if params[:ticker].include?(":")
symbol, mic_code = params[:ticker].split(":")
@stock = Stock.find_by(symbol:, mic_code:)
else
@stock = Stock.find_by(symbol: params[:ticker], country_code: "US")
end
end

def exchanges
if params[:id]
@exchange = params[:id]
@stocks = Stock.where(exchange: @exchange).where.not(mic_code: nil).order(:name)
@pagy, @stocks = pagy(@stocks, limit: 27, size: [ 1, 3, 3, 1 ])
render :all
else
@exchanges = Stock.where(kind: "stock").where.not(mic_code: nil).distinct.pluck(:exchange).compact.sort
end
end

def industries
if params[:id]
@industry = params[:id]
@stocks = Stock.where(industry: @industry).where.not(mic_code: nil).order(:name)
@pagy, @stocks = pagy(@stocks, limit: 27, size: [ 1, 3, 3, 1 ])
render :all
else
@industries = Stock.where(kind: "stock").where.not(mic_code: nil).distinct.pluck(:industry).compact.sort
end
end

def sectors
if params[:id]
@sector = sector_from_slug(params[:id])
if @sector
@stocks = Stock.where(sector: @sector).where.not(mic_code: nil).order(:name)
@pagy, @stocks = pagy(@stocks, limit: 27, size: [ 1, 3, 3, 1 ])
render :all
else
redirect_to stocks_path, status: :moved_permanently
end
else
@sectors = Stock.where(kind: "stock").where.not(mic_code: nil).distinct.pluck(:sector).compact.sort
end
end

private

def sector_from_slug(slug)
Stock.where(kind: "stock")
.where.not(mic_code: nil)
.distinct
.pluck(:sector)
.compact
.find { |sector| sector_slug(sector) == slug }
end

def sector_slug(sector)
sector.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/-+$/, "")
end
end
12 changes: 12 additions & 0 deletions app/helpers/stocks_helper.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
module StocksHelper
def sector_slug(sector)
sector.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/-+$/, "")
end

def sector_display_name(slug)
Stock.where(kind: "stock")
.where.not(mic_code: nil)
.distinct
.pluck(:sector)
.compact
.find { |sector| sector_slug(sector) == slug }
end
end
40 changes: 20 additions & 20 deletions app/javascript/controllers/stock_chart_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ export default class extends Controller {

const svg = d3.select(chartElement)
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
.attr("transform", `translate(${margin.left},${margin.top})`)

const x = d3.scaleTime()
.domain(d3.extent(prices, d => new Date(d.date)))
Expand All @@ -76,11 +76,11 @@ export default class extends Controller {
.attr("y1", y(d3.min(prices, d => d.low)))
.attr("x2", 0)
.attr("y2", y(d3.max(prices, d => d.high)))

gradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", "#10B981")

gradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", "#10B981")
Expand Down Expand Up @@ -152,18 +152,18 @@ export default class extends Controller {
const d1 = prices[i]
const d = x0 - new Date(d0.date) > new Date(d1.date) - x0 ? d1 : d0
const prevD = prices[Math.max(0, i - 1)]

const xPos = x(new Date(d.date))
const yPos = y(d.close)

focus.attr("transform", `translate(${xPos},0)`)
focus.select("circle").attr("cy", yPos)
focus.select("line").attr("y2", height)

const change = d.close - prevD.close
const changePercent = ((change / prevD.close) * 100).toFixed(2)
const changeText = `${change >= 0 ? '+' : ''}$${change.toFixed(2)} (${changePercent}%)`

tooltip.html(`
<div class="font-semibold">${d3.timeFormat("%b %d, %Y")(new Date(d.date))}</div>
<div>$${d.close.toFixed(2)}</div>
Expand Down Expand Up @@ -203,7 +203,7 @@ export default class extends Controller {

updateTimeRange(event) {
const timeRange = event.target.dataset.timeRange;

this.element.querySelectorAll('button').forEach(btn => {
btn.classList.remove('bg-gray-50', 'text-gray-700')
btn.classList.add('bg-transparent', 'text-gray-500')
Expand All @@ -215,21 +215,21 @@ export default class extends Controller {
this.element.querySelector('#stock-chart').classList.add('hidden')

const url = `/stocks/${this.symbolValue}/chart?time_range=${timeRange}`

fetch(url, {
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
this.dataValue = data
this.drawChart()
})
.catch(error => {
this.loadingSpinnerTarget.classList.add('hidden')
this.element.querySelector('#stock-chart').innerHTML = '<p class="text-red-500">Error loading chart data. Please try again.</p>'
})
.then(response => response.json())
.then(data => {
this.dataValue = data
this.drawChart()
})
.catch(error => {
this.loadingSpinnerTarget.classList.add('hidden')
this.element.querySelector('#stock-chart').innerHTML = '<p class="text-red-500">Error loading chart data. Please try again.</p>'
})
}
}
Loading

0 comments on commit 3590458

Please sign in to comment.