mirror of
https://github.com/we-promise/sure.git
synced 2026-04-18 19:44:09 +00:00
Remove Synth Finance references (#47)
* Remove Synth Finance integration * Linter noise * Fix failing (old) test, use it for Twelve Data --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
@@ -18,8 +18,7 @@ This rule serves as high-level documentation for how you should write code for t
|
||||
- Jobs: Sidekiq + Redis
|
||||
- External
|
||||
- Payments: Stripe
|
||||
- User bank data syncing: Plaid
|
||||
- Market data: Synth (our custom API)
|
||||
- User bank data syncing: Plaid
|
||||
|
||||
## Project conventions
|
||||
|
||||
|
||||
@@ -158,20 +158,11 @@ app/models/
|
||||
registry.rb <- Defines available providers by concept
|
||||
concepts/
|
||||
exchange_rate.rb <- defines the interface required for the exchange rate concept
|
||||
synth.rb # <- Concrete provider implementation
|
||||
```
|
||||
|
||||
### One-off data
|
||||
|
||||
For data that does not fit neatly into a "concept", an interface is not required and the concrete provider may implement ad-hoc methods called directly in code. For example, the [synth.rb](mdc:app/models/provider/synth.rb) provider has a `usage` method that is only applicable to this specific provider. This should be called directly without any abstractions:
|
||||
|
||||
```rb
|
||||
class SomeModel < Application
|
||||
def synth_usage
|
||||
Provider::Registry.get_provider(:synth)&.usage
|
||||
end
|
||||
end
|
||||
```
|
||||
For data that does not fit neatly into a "concept", an interface is not required and the concrete provider may implement ad-hoc methods called directly in code.
|
||||
|
||||
## "Provided" Concerns
|
||||
|
||||
|
||||
@@ -17,10 +17,6 @@ SECRET_KEY_BASE=secret-value
|
||||
# Optional self-hosting vars
|
||||
# --------------------------------------------------------------------------------------------------------
|
||||
|
||||
# Optional: Synth API Key for exchange rates + stock prices
|
||||
# (you can also set this in your self-hosted settings page)
|
||||
# Get it here: https://synthfinance.com/
|
||||
SYNTH_API_KEY=
|
||||
|
||||
# Optional: Twelve Data API Key for exchange rates + stock prices
|
||||
# (you can also set this in your self-hosted settings page)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# To enable / disable self-hosting features.
|
||||
SELF_HOSTED=false
|
||||
|
||||
# Enable Synth market data (careful, this will use your API credits)
|
||||
SYNTH_API_KEY=yourapikeyhere
|
||||
# Enable Twelve market data (careful, this will use your API credits)
|
||||
TWELVE_DATA_API_KEY=yourapikeyhere
|
||||
|
||||
@@ -6,7 +6,6 @@ SELF_HOSTED=false
|
||||
# Uncomment and fill in live keys when you need to generate a VCR cassette fixture
|
||||
# ================
|
||||
|
||||
# SYNTH_API_KEY=<add live key here>
|
||||
|
||||
# ================
|
||||
# Miscellaneous
|
||||
|
||||
@@ -115,7 +115,6 @@ Sidekiq handles asynchronous tasks:
|
||||
|
||||
### Multi-Currency Support
|
||||
- All monetary values stored in base currency (user's primary currency)
|
||||
- Exchange rates fetched from Synth API
|
||||
- `Money` objects handle currency conversion and formatting
|
||||
- Historical exchange rates for accurate reporting
|
||||
|
||||
|
||||
@@ -6,9 +6,6 @@ class Settings::HostingsController < ApplicationController
|
||||
before_action :ensure_admin, only: :clear_cache
|
||||
|
||||
def show
|
||||
synth_provider = Provider::Registry.get_provider(:synth)
|
||||
@synth_usage = synth_provider&.usage
|
||||
|
||||
twelve_data_provider = Provider::Registry.get_provider(:twelve_data)
|
||||
@twelve_data_usage = twelve_data_provider&.usage
|
||||
end
|
||||
@@ -22,10 +19,6 @@ class Settings::HostingsController < ApplicationController
|
||||
Setting.require_email_confirmation = hosting_params[:require_email_confirmation]
|
||||
end
|
||||
|
||||
if hosting_params.key?(:synth_api_key)
|
||||
Setting.synth_api_key = hosting_params[:synth_api_key]
|
||||
end
|
||||
|
||||
if hosting_params.key?(:twelve_data_api_key)
|
||||
Setting.twelve_data_api_key = hosting_params[:twelve_data_api_key]
|
||||
end
|
||||
@@ -43,7 +36,7 @@ class Settings::HostingsController < ApplicationController
|
||||
|
||||
private
|
||||
def hosting_params
|
||||
params.require(:setting).permit(:require_invite_for_signup, :require_email_confirmation, :synth_api_key, :twelve_data_api_key)
|
||||
params.require(:setting).permit(:require_invite_for_signup, :require_email_confirmation, :twelve_data_api_key)
|
||||
end
|
||||
|
||||
def ensure_admin
|
||||
|
||||
@@ -36,9 +36,7 @@ class Family::AutoMerchantDetector
|
||||
source: "ai",
|
||||
name: auto_detection.business_name,
|
||||
website_url: auto_detection.business_url,
|
||||
) do |pm|
|
||||
pm.logo_url = "#{default_logo_provider_url}/#{auto_detection.business_url}"
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
merchant_id = merchant_id || ai_provider_merchant&.id
|
||||
@@ -65,9 +63,6 @@ class Family::AutoMerchantDetector
|
||||
Provider::Registry.get_provider(:openai)
|
||||
end
|
||||
|
||||
def default_logo_provider_url
|
||||
"https://logo.synthfinance.com"
|
||||
end
|
||||
|
||||
def user_merchants_input
|
||||
family.merchants.map do |merchant|
|
||||
|
||||
@@ -32,14 +32,6 @@ class Provider::Registry
|
||||
Provider::Stripe.new(secret_key:, webhook_secret:)
|
||||
end
|
||||
|
||||
def synth
|
||||
api_key = ENV.fetch("SYNTH_API_KEY", Setting.synth_api_key)
|
||||
|
||||
return nil unless api_key.present?
|
||||
|
||||
Provider::Synth.new(api_key)
|
||||
end
|
||||
|
||||
def twelve_data
|
||||
api_key = ENV.fetch("TWELVE_DATA_API_KEY", Setting.twelve_data_api_key)
|
||||
|
||||
@@ -100,13 +92,13 @@ class Provider::Registry
|
||||
def available_providers
|
||||
case concept
|
||||
when :exchange_rates
|
||||
%i[synth twelve_data]
|
||||
%i[twelve_data]
|
||||
when :securities
|
||||
%i[synth twelve_data]
|
||||
%i[twelve_data]
|
||||
when :llm
|
||||
%i[openai]
|
||||
else
|
||||
%i[synth plaid_us plaid_eu github openai]
|
||||
%i[plaid_us plaid_eu github openai]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
class Provider::Synth < Provider
|
||||
include ExchangeRateConcept, SecurityConcept
|
||||
|
||||
# Subclass so errors caught in this provider are raised as Provider::Synth::Error
|
||||
Error = Class.new(Provider::Error)
|
||||
InvalidExchangeRateError = Class.new(Error)
|
||||
InvalidSecurityPriceError = Class.new(Error)
|
||||
|
||||
def initialize(api_key)
|
||||
@api_key = api_key
|
||||
end
|
||||
|
||||
def healthy?
|
||||
with_provider_response do
|
||||
response = client.get("#{base_url}/user")
|
||||
JSON.parse(response.body).dig("id").present?
|
||||
end
|
||||
end
|
||||
|
||||
def usage
|
||||
with_provider_response do
|
||||
response = client.get("#{base_url}/user")
|
||||
|
||||
parsed = JSON.parse(response.body)
|
||||
|
||||
remaining = parsed.dig("api_calls_remaining")
|
||||
limit = parsed.dig("api_limit")
|
||||
used = limit - remaining
|
||||
|
||||
UsageData.new(
|
||||
used: used,
|
||||
limit: limit,
|
||||
utilization: used.to_f / limit * 100,
|
||||
plan: parsed.dig("plan"),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# ================================
|
||||
# Exchange Rates
|
||||
# ================================
|
||||
|
||||
def fetch_exchange_rate(from:, to:, date:)
|
||||
with_provider_response do
|
||||
response = client.get("#{base_url}/rates/historical") do |req|
|
||||
req.params["date"] = date.to_s
|
||||
req.params["from"] = from
|
||||
req.params["to"] = to
|
||||
end
|
||||
|
||||
rates = JSON.parse(response.body).dig("data", "rates")
|
||||
|
||||
Rate.new(date: date.to_date, from:, to:, rate: rates.dig(to))
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_exchange_rates(from:, to:, start_date:, end_date:)
|
||||
with_provider_response do
|
||||
data = paginate(
|
||||
"#{base_url}/rates/historical-range",
|
||||
from: from,
|
||||
to: to,
|
||||
date_start: start_date.to_s,
|
||||
date_end: end_date.to_s
|
||||
) do |body|
|
||||
body.dig("data")
|
||||
end
|
||||
|
||||
data.paginated.map do |rate|
|
||||
date = rate.dig("date")
|
||||
rate = rate.dig("rates", to)
|
||||
|
||||
if date.nil? || rate.nil?
|
||||
Rails.logger.warn("#{self.class.name} returned invalid rate data for pair from: #{from} to: #{to} on: #{date}. Rate data: #{rate.inspect}")
|
||||
Sentry.capture_exception(InvalidExchangeRateError.new("#{self.class.name} returned invalid rate data"), level: :warning) do |scope|
|
||||
scope.set_context("rate", { from: from, to: to, date: date })
|
||||
end
|
||||
|
||||
next
|
||||
end
|
||||
|
||||
Rate.new(date: date.to_date, from:, to:, rate:)
|
||||
end.compact
|
||||
end
|
||||
end
|
||||
|
||||
# ================================
|
||||
# Securities
|
||||
# ================================
|
||||
|
||||
def search_securities(symbol, country_code: nil, exchange_operating_mic: nil)
|
||||
with_provider_response do
|
||||
response = client.get("#{base_url}/tickers/search") do |req|
|
||||
req.params["name"] = symbol
|
||||
req.params["dataset"] = "limited"
|
||||
req.params["country_code"] = country_code if country_code.present?
|
||||
# Synth uses mic_code, which encompasses both exchange_mic AND exchange_operating_mic (union)
|
||||
req.params["mic_code"] = exchange_operating_mic if exchange_operating_mic.present?
|
||||
req.params["limit"] = 25
|
||||
end
|
||||
|
||||
parsed = JSON.parse(response.body)
|
||||
|
||||
parsed.dig("data").map do |security|
|
||||
Security.new(
|
||||
symbol: security.dig("symbol"),
|
||||
name: security.dig("name"),
|
||||
logo_url: security.dig("logo_url"),
|
||||
exchange_operating_mic: security.dig("exchange", "operating_mic_code"),
|
||||
country_code: security.dig("exchange", "country_code")
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_security_info(symbol:, exchange_operating_mic:)
|
||||
with_provider_response do
|
||||
response = client.get("#{base_url}/tickers/#{symbol}") do |req|
|
||||
req.params["operating_mic"] = exchange_operating_mic
|
||||
end
|
||||
|
||||
data = JSON.parse(response.body).dig("data")
|
||||
|
||||
SecurityInfo.new(
|
||||
symbol: symbol,
|
||||
name: data.dig("name"),
|
||||
links: data.dig("links"),
|
||||
logo_url: data.dig("logo_url"),
|
||||
description: data.dig("description"),
|
||||
kind: data.dig("kind"),
|
||||
exchange_operating_mic: exchange_operating_mic
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_security_price(symbol:, exchange_operating_mic: nil, date:)
|
||||
with_provider_response do
|
||||
historical_data = fetch_security_prices(symbol:, exchange_operating_mic:, start_date: date, end_date: date)
|
||||
|
||||
raise ProviderError, "No prices found for security #{symbol} on date #{date}" if historical_data.data.empty?
|
||||
|
||||
historical_data.data.first
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_security_prices(symbol:, exchange_operating_mic: nil, start_date:, end_date:)
|
||||
with_provider_response do
|
||||
params = {
|
||||
start_date: start_date,
|
||||
end_date: end_date,
|
||||
operating_mic_code: exchange_operating_mic
|
||||
}.compact
|
||||
|
||||
data = paginate(
|
||||
"#{base_url}/tickers/#{symbol}/open-close",
|
||||
params
|
||||
) do |body|
|
||||
body.dig("prices")
|
||||
end
|
||||
|
||||
currency = data.first_page.dig("currency")
|
||||
exchange_operating_mic = data.first_page.dig("exchange", "operating_mic_code")
|
||||
|
||||
data.paginated.map do |price|
|
||||
date = price.dig("date")
|
||||
price = price.dig("close") || price.dig("open")
|
||||
|
||||
if date.nil? || price.nil?
|
||||
Rails.logger.warn("#{self.class.name} returned invalid price data for security #{symbol} on: #{date}. Price data: #{price.inspect}")
|
||||
Sentry.capture_exception(InvalidSecurityPriceError.new("#{self.class.name} returned invalid security price data"), level: :warning) do |scope|
|
||||
scope.set_context("security", { symbol: symbol, date: date })
|
||||
end
|
||||
|
||||
next
|
||||
end
|
||||
|
||||
Price.new(
|
||||
symbol: symbol,
|
||||
date: date.to_date,
|
||||
price: price,
|
||||
currency: currency,
|
||||
exchange_operating_mic: exchange_operating_mic
|
||||
)
|
||||
end.compact
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :api_key
|
||||
|
||||
def base_url
|
||||
ENV["SYNTH_URL"] || "https://api.synthfinance.com"
|
||||
end
|
||||
|
||||
def app_name
|
||||
"maybe_app"
|
||||
end
|
||||
|
||||
def app_type
|
||||
Rails.application.config.app_mode
|
||||
end
|
||||
|
||||
def client
|
||||
@client ||= Faraday.new(url: base_url) do |faraday|
|
||||
faraday.request(:retry, {
|
||||
max: 2,
|
||||
interval: 0.05,
|
||||
interval_randomness: 0.5,
|
||||
backoff_factor: 2
|
||||
})
|
||||
|
||||
faraday.response :raise_error
|
||||
faraday.headers["Authorization"] = "Bearer #{api_key}"
|
||||
faraday.headers["X-Source"] = app_name
|
||||
faraday.headers["X-Source-Type"] = app_type
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_page(url, page, params = {})
|
||||
client.get(url, params.merge(page: page))
|
||||
end
|
||||
|
||||
def paginate(url, params = {})
|
||||
results = []
|
||||
page = 1
|
||||
current_page = 0
|
||||
total_pages = 1
|
||||
first_page = nil
|
||||
|
||||
while current_page < total_pages
|
||||
response = fetch_page(url, page, params)
|
||||
|
||||
body = JSON.parse(response.body)
|
||||
first_page = body unless first_page
|
||||
page_results = yield(body)
|
||||
results.concat(page_results)
|
||||
|
||||
current_page = body.dig("paging", "current_page")
|
||||
total_pages = body.dig("paging", "total_pages")
|
||||
|
||||
page += 1
|
||||
end
|
||||
|
||||
PaginatedData.new(
|
||||
paginated: results,
|
||||
first_page: first_page,
|
||||
total_pages: total_pages
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -18,7 +18,7 @@ class Security < ApplicationRecord
|
||||
end
|
||||
|
||||
def to_combobox_option
|
||||
SynthComboboxOption.new(
|
||||
ComboboxOption.new(
|
||||
symbol: ticker,
|
||||
name: name,
|
||||
logo_url: logo_url,
|
||||
|
||||
13
app/models/security/combobox_option.rb
Normal file
13
app/models/security/combobox_option.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
class Security::ComboboxOption
|
||||
include ActiveModel::Model
|
||||
|
||||
attr_accessor :symbol, :name, :logo_url, :exchange_operating_mic, :country_code
|
||||
|
||||
def id
|
||||
"#{symbol}|#{exchange_operating_mic}"
|
||||
end
|
||||
|
||||
def to_combobox_display
|
||||
"#{symbol} - #{name} (#{exchange_operating_mic})"
|
||||
end
|
||||
end
|
||||
@@ -1,13 +0,0 @@
|
||||
class Security::SynthComboboxOption
|
||||
include ActiveModel::Model
|
||||
|
||||
attr_accessor :symbol, :name, :logo_url, :exchange_operating_mic, :country_code
|
||||
|
||||
def id
|
||||
"#{symbol}|#{exchange_operating_mic}" # submitted by combobox as value
|
||||
end
|
||||
|
||||
def to_combobox_display
|
||||
"#{symbol} - #{name} (#{exchange_operating_mic})" # shown in combobox input when selected
|
||||
end
|
||||
end
|
||||
@@ -2,7 +2,6 @@
|
||||
class Setting < RailsSettings::Base
|
||||
cache_prefix { "v1" }
|
||||
|
||||
field :synth_api_key, type: :string, default: ENV["SYNTH_API_KEY"]
|
||||
field :twelve_data_api_key, type: :string, default: ENV["TWELVE_DATA_API_KEY"]
|
||||
field :openai_access_token, type: :string, default: ENV["OPENAI_ACCESS_TOKEN"]
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class Trade::CreateForm
|
||||
end
|
||||
|
||||
private
|
||||
# Users can either look up a ticker from our provider (Synth) or enter a manual, "offline" ticker (that we won't fetch prices for)
|
||||
# Users can either look up a ticker from a provider or enter a manual, "offline" ticker (that we won't fetch prices for)
|
||||
def security
|
||||
ticker_symbol, exchange_operating_mic = ticker.present? ? ticker.split("|") : [ manual_ticker, nil ]
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
"full" => "w-full h-full"
|
||||
} %>
|
||||
|
||||
<% if account.plaid_account_id? && account.institution_domain.present? %>
|
||||
<%= image_tag "https://logo.synthfinance.com/#{account.institution_domain}", class: "shrink-0 rounded-full #{size_classes[size]}" %>
|
||||
<% elsif account.logo.attached? %>
|
||||
<% if account.logo.attached? %>
|
||||
<%= image_tag account.logo, class: "shrink-0 rounded-full #{size_classes[size]}" %>
|
||||
<% else %>
|
||||
<%= render DS::FilledIcon.new(variant: :text, hex_color: color || account.accountable.color, text: account.name, size: size, rounded: true) %>
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<%= turbo_frame_tag dom_id(holding) do %>
|
||||
<div class="grid grid-cols-12 items-center text-primary text-sm font-medium p-4">
|
||||
<div class="col-span-4 flex items-center gap-4">
|
||||
<%= image_tag "https://logo.synthfinance.com/ticker/#{holding.ticker}", class: "w-9 h-9 rounded-full", loading: "lazy" %>
|
||||
<% if holding.security.logo_url.present? %>
|
||||
<%= image_tag holding.security.logo_url, class: "w-9 h-9 rounded-full", loading: "lazy" %>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-0.5">
|
||||
<%= link_to holding.name, holding_path(holding), data: { turbo_frame: :drawer }, class: "hover:underline" %>
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
<%= tag.p @holding.ticker, class: "text-sm text-secondary" %>
|
||||
</div>
|
||||
|
||||
<%= image_tag "https://logo.synthfinance.com/ticker/#{@holding.ticker}", loading: "lazy", class: "w-9 h-9 rounded-full" %>
|
||||
<% if @holding.security.logo_url.present? %>
|
||||
<%= image_tag @holding.security.logo_url, loading: "lazy", class: "w-9 h-9 rounded-full" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h2 class="font-medium mb-1"><%= t(".title") %></h2>
|
||||
<% if ENV["SYNTH_API_KEY"].present? %>
|
||||
<p class="text-sm text-secondary">You have successfully configured your Synth API key through the SYNTH_API_KEY environment variable.</p>
|
||||
<% else %>
|
||||
<p class="text-secondary text-sm mb-4"><%= t(".description") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= styled_form_with model: Setting.new,
|
||||
url: settings_hosting_path,
|
||||
method: :patch,
|
||||
data: {
|
||||
controller: "auto-submit-form",
|
||||
"auto-submit-form-trigger-event-value": "blur"
|
||||
} do |form| %>
|
||||
<%= form.text_field :synth_api_key,
|
||||
label: t(".label"),
|
||||
type: "password",
|
||||
placeholder: t(".placeholder"),
|
||||
value: ENV.fetch("SYNTH_API_KEY", Setting.synth_api_key),
|
||||
disabled: ENV["SYNTH_API_KEY"].present?,
|
||||
container_class: @synth_usage.present? && !@synth_usage.success? ? "border-red-500" : "",
|
||||
data: { "auto-submit-form-target": "auto" } %>
|
||||
<% end %>
|
||||
|
||||
<% if @synth_usage.present? && @synth_usage.success? %>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm text-secondary">
|
||||
<%= t(".api_calls_used",
|
||||
used: number_with_delimiter(@synth_usage.data.used),
|
||||
limit: number_with_delimiter(@synth_usage.data.limit),
|
||||
percentage: number_to_percentage(@synth_usage.data.utilization, precision: 1)) %>
|
||||
</p>
|
||||
<div class="w-52 h-1.5 bg-gray-100 rounded-2xl">
|
||||
<div class="h-full bg-green-500 rounded-2xl"
|
||||
style="width: <%= [@synth_usage.data.utilization, 2].max %>%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-100 rounded-md px-1.5 py-0.5 w-fit">
|
||||
<p class="text-xs font-medium text-secondary uppercase">
|
||||
<%= t(".plan", plan: @synth_usage.data.plan) %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -1,9 +1,6 @@
|
||||
<%= content_for :page_title, t(".title") %>
|
||||
|
||||
<%= settings_section title: t(".general") do %>
|
||||
<div class="space-y-6">
|
||||
<%= render "settings/hostings/synth_settings" %>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<%= render "settings/hostings/twelve_data_settings" %>
|
||||
</div>
|
||||
|
||||
@@ -21,13 +21,6 @@ en:
|
||||
confirm_clear_cache:
|
||||
title: Clear data cache?
|
||||
body: Are you sure you want to clear the data cache? This will remove all exchange rates, security prices, account balances, and other data. This action cannot be undone.
|
||||
synth_settings:
|
||||
api_calls_used: "%{used} / %{limit} API calls used (%{percentage})"
|
||||
description: Input the API key provided by Synth
|
||||
label: API Key
|
||||
placeholder: Enter your API key here
|
||||
plan: "%{plan} plan"
|
||||
title: Synth Settings
|
||||
twelve_data_settings:
|
||||
api_calls_used: "%{used} / %{limit} API daily calls used (%{percentage})"
|
||||
description: Input the API key provided by Twelve Data
|
||||
|
||||
@@ -20,13 +20,6 @@ nb:
|
||||
confirm_clear_cache:
|
||||
title: Tøm cache?
|
||||
body: Er du sikker på at du vil tømme cache? Dette vil fjerne alle valutakurser, verdipapirpriser, kontobalanser og andre data. Denne handlingen kan ikke angres.
|
||||
synth_settings:
|
||||
api_calls_used: "%{used} / %{limit} API-kall brukt (%{percentage})"
|
||||
description: Angi API-nøkkelen som er gitt av Synth
|
||||
label: API-nøkkel
|
||||
placeholder: Angi API-nøkkelen din her
|
||||
plan: "%{plan} plan"
|
||||
title: Synth-innstillinger
|
||||
update:
|
||||
failure: Ugyldig innstillingsverdi
|
||||
success: Innstillinger oppdatert
|
||||
|
||||
@@ -1,64 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
namespace :securities do
|
||||
desc "Backfill exchange_operating_mic for securities using Synth API"
|
||||
task backfill_exchange_mic: :environment do
|
||||
puts "Starting exchange_operating_mic backfill..."
|
||||
|
||||
api_key = Rails.application.config.app_mode.self_hosted? ? Setting.synth_api_key : ENV["SYNTH_API_KEY"]
|
||||
unless api_key.present?
|
||||
puts "ERROR: No Synth API key found. Please set SYNTH_API_KEY env var or configure it in Settings for self-hosted mode."
|
||||
exit 1
|
||||
end
|
||||
|
||||
securities = Security.where(exchange_operating_mic: nil).where.not(ticker: nil)
|
||||
total = securities.count
|
||||
processed = 0
|
||||
errors = []
|
||||
|
||||
securities.find_each do |security|
|
||||
processed += 1
|
||||
print "\rProcessing #{processed}/#{total} (#{(processed.to_f/total * 100).round(1)}%)"
|
||||
|
||||
begin
|
||||
response = Faraday.get("https://api.synthfinance.com/tickers/#{security.ticker}") do |req|
|
||||
req.params["country_code"] = security.country_code if security.country_code.present?
|
||||
req.headers["Authorization"] = "Bearer #{api_key}"
|
||||
end
|
||||
|
||||
if response.success?
|
||||
data = JSON.parse(response.body).dig("data")
|
||||
exchange_data = data["exchange"]
|
||||
|
||||
# Update security with exchange info and other metadata
|
||||
security.update!(
|
||||
exchange_operating_mic: exchange_data["operating_mic_code"],
|
||||
exchange_mic: exchange_data["mic_code"],
|
||||
exchange_acronym: exchange_data["acronym"],
|
||||
name: data["name"],
|
||||
logo_url: data["logo_url"],
|
||||
country_code: exchange_data["country_code"]
|
||||
)
|
||||
else
|
||||
errors << "#{security.ticker}: HTTP #{response.status} - #{response.body}"
|
||||
end
|
||||
rescue => e
|
||||
errors << "#{security.ticker}: #{e.message}"
|
||||
end
|
||||
|
||||
# Add a small delay to not overwhelm the API
|
||||
sleep(0.1)
|
||||
end
|
||||
|
||||
puts "\n\nBackfill complete!"
|
||||
puts "Processed #{processed} securities"
|
||||
|
||||
if errors.any?
|
||||
puts "\nErrors encountered:"
|
||||
errors.each { |error| puts " - #{error}" }
|
||||
end
|
||||
end
|
||||
|
||||
desc "De-duplicate securities based on ticker + exchange_operating_mic"
|
||||
task :deduplicate, [ :dry_run ] => :environment do |_t, args|
|
||||
dry_run = args[:dry_run].present?
|
||||
|
||||
@@ -8,7 +8,6 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
|
||||
sign_in users(:family_admin)
|
||||
|
||||
@provider = mock
|
||||
Provider::Registry.stubs(:get_provider).with(:synth).returns(@provider)
|
||||
Provider::Registry.stubs(:get_provider).with(:twelve_data).returns(@provider)
|
||||
@usage_response = provider_success_response(
|
||||
OpenStruct.new(
|
||||
@@ -32,7 +31,6 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
test "should get edit when self hosting is enabled" do
|
||||
@provider.expects(:usage).returns(@usage_response)
|
||||
@provider.expects(:usage).returns(@usage_response)
|
||||
|
||||
with_self_hosting do
|
||||
get settings_hosting_url
|
||||
@@ -42,9 +40,9 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
test "can update settings when self hosting is enabled" do
|
||||
with_self_hosting do
|
||||
patch settings_hosting_url, params: { setting: { synth_api_key: "1234567890" } }
|
||||
patch settings_hosting_url, params: { setting: { twelve_data_api_key: "1234567890" } }
|
||||
|
||||
assert_equal "1234567890", Setting.synth_api_key
|
||||
assert_equal "1234567890", Setting.twelve_data_api_key
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ module SecurityProviderInterfaceTest
|
||||
|
||||
assert response.success?
|
||||
assert response.data.first.date.is_a?(Date)
|
||||
assert_equal 147, response.data.count # Synth won't return prices on weekends / holidays, so less than total day count of 213
|
||||
assert_equal 147, response.data.count
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ class Family::AutoMerchantDetectorTest < ActiveSupport::TestCase
|
||||
|
||||
assert_equal "McDonalds", txn1.reload.merchant.name
|
||||
assert_equal "Chipotle", txn2.reload.merchant.name
|
||||
assert_equal "https://logo.synthfinance.com/mcdonalds.com", txn1.reload.merchant.logo_url
|
||||
assert_equal "https://logo.synthfinance.com/chipotle.com", txn2.reload.merchant.logo_url
|
||||
assert_nil txn1.reload.merchant.logo_url
|
||||
assert_nil txn2.reload.merchant.logo_url
|
||||
assert_nil txn3.reload.merchant
|
||||
|
||||
# After auto-detection, all transactions are locked and no longer enrichable
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class Provider::RegistryTest < ActiveSupport::TestCase
|
||||
test "synth configured with ENV" do
|
||||
Setting.stubs(:synth_api_key).returns(nil)
|
||||
|
||||
with_env_overrides SYNTH_API_KEY: "123" do
|
||||
assert_instance_of Provider::Synth, Provider::Registry.get_provider(:synth)
|
||||
end
|
||||
end
|
||||
|
||||
test "synth configured with Setting" do
|
||||
Setting.stubs(:synth_api_key).returns("123")
|
||||
|
||||
with_env_overrides SYNTH_API_KEY: nil do
|
||||
assert_instance_of Provider::Synth, Provider::Registry.get_provider(:synth)
|
||||
end
|
||||
end
|
||||
|
||||
test "synth not configured" do
|
||||
Setting.stubs(:synth_api_key).returns(nil)
|
||||
|
||||
with_env_overrides SYNTH_API_KEY: nil do
|
||||
assert_nil Provider::Registry.get_provider(:synth)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,26 +0,0 @@
|
||||
require "test_helper"
|
||||
require "ostruct"
|
||||
|
||||
class Provider::SynthTest < ActiveSupport::TestCase
|
||||
include ExchangeRateProviderInterfaceTest, SecurityProviderInterfaceTest
|
||||
|
||||
setup do
|
||||
@subject = @synth = Provider::Synth.new(ENV["SYNTH_API_KEY"])
|
||||
end
|
||||
|
||||
test "health check" do
|
||||
VCR.use_cassette("synth/health") do
|
||||
assert @synth.healthy?
|
||||
end
|
||||
end
|
||||
|
||||
test "usage info" do
|
||||
VCR.use_cassette("synth/usage") do
|
||||
usage = @synth.usage.data
|
||||
assert usage.used.present?
|
||||
assert usage.limit.present?
|
||||
assert usage.utilization.present?
|
||||
assert usage.plan.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -33,7 +33,6 @@ class SettingsTest < ApplicationSystemTestCase
|
||||
|
||||
test "can update self hosting settings" do
|
||||
Rails.application.config.app_mode.stubs(:self_hosted?).returns(true)
|
||||
Provider::Registry.stubs(:get_provider).with(:synth).returns(nil)
|
||||
Provider::Registry.stubs(:get_provider).with(:twelve_data).returns(nil)
|
||||
open_settings_from_sidebar
|
||||
assert_selector "li", text: "Self hosting"
|
||||
|
||||
@@ -28,7 +28,6 @@ VCR.configure do |config|
|
||||
config.hook_into :webmock
|
||||
config.ignore_localhost = true
|
||||
config.default_cassette_options = { erb: true }
|
||||
config.filter_sensitive_data("<SYNTH_API_KEY>") { ENV["SYNTH_API_KEY"] }
|
||||
config.filter_sensitive_data("<OPENAI_ACCESS_TOKEN>") { ENV["OPENAI_ACCESS_TOKEN"] }
|
||||
config.filter_sensitive_data("<OPENAI_ORGANIZATION_ID>") { ENV["OPENAI_ORGANIZATION_ID"] }
|
||||
config.filter_sensitive_data("<STRIPE_SECRET_KEY>") { ENV["STRIPE_SECRET_KEY"] }
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://api.synthfinance.com/rates/historical?date=2024-01-01&from=USD&to=GBP
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ''
|
||||
headers:
|
||||
Authorization:
|
||||
- Bearer <SYNTH_API_KEY>
|
||||
X-Source:
|
||||
- maybe_app
|
||||
X-Source-Type:
|
||||
- managed
|
||||
User-Agent:
|
||||
- Faraday v2.13.1
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Date:
|
||||
- Fri, 16 May 2025 13:01:38 GMT
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Connection:
|
||||
- keep-alive
|
||||
Cache-Control:
|
||||
- max-age=0, private, must-revalidate
|
||||
Etag:
|
||||
- W/"0c93a67d0c68e6f206e2954a41aa2933"
|
||||
Referrer-Policy:
|
||||
- strict-origin-when-cross-origin
|
||||
Rndr-Id:
|
||||
- 146e30b2-e03b-47e3
|
||||
Strict-Transport-Security:
|
||||
- max-age=63072000; includeSubDomains
|
||||
Vary:
|
||||
- Accept-Encoding
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Permitted-Cross-Domain-Policies:
|
||||
- none
|
||||
X-Render-Origin-Server:
|
||||
- Render
|
||||
X-Request-Id:
|
||||
- 3cf7ade1-8066-422a-97c7-5f8b99e24296
|
||||
X-Runtime:
|
||||
- '0.024284'
|
||||
X-Xss-Protection:
|
||||
- '0'
|
||||
Cf-Cache-Status:
|
||||
- DYNAMIC
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=ih8sEFqAOWyINqAEtKGKPKO2lr1qAYSVeipyB5F8g2umPODXvCD4hN3G6wTTs2Q7H8CDWsqiOlYkmVvmr%2BWvl2ojOtBwO25Ahk9TbhlcgRO9nT6mEIXOSdVXJpzpRn5Ov%2FMGigpQ"}],"group":"cf-nel","max_age":604800}'
|
||||
Nel:
|
||||
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
|
||||
Speculation-Rules:
|
||||
- '"/cdn-cgi/speculation"'
|
||||
Server:
|
||||
- cloudflare
|
||||
Cf-Ray:
|
||||
- 940b109b5df1a3d7-ORD
|
||||
Alt-Svc:
|
||||
- h3=":443"; ma=86400
|
||||
Server-Timing:
|
||||
- cfL4;desc="?proto=TCP&rtt=25865&min_rtt=25683&rtt_var=9996&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2827&recv_bytes=922&delivery_rate=106690&cwnd=219&unsent_bytes=0&cid=e48ae188d1f86721&ts=190&x=0"
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"data":{"date":"2024-01-01","source":"USD","rates":{"GBP":0.785476}},"meta":{"total_records":1,"credits_used":1,"credits_remaining":249734,"date":"2024-01-01"}}'
|
||||
recorded_at: Fri, 16 May 2025 13:01:38 GMT
|
||||
recorded_with: VCR 6.3.1
|
||||
File diff suppressed because one or more lines are too long
@@ -1,82 +0,0 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://api.synthfinance.com/user
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ''
|
||||
headers:
|
||||
Authorization:
|
||||
- Bearer <SYNTH_API_KEY>
|
||||
X-Source:
|
||||
- maybe_app
|
||||
X-Source-Type:
|
||||
- managed
|
||||
User-Agent:
|
||||
- Faraday v2.13.1
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Date:
|
||||
- Fri, 16 May 2025 13:01:39 GMT
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Connection:
|
||||
- keep-alive
|
||||
Cache-Control:
|
||||
- max-age=0, private, must-revalidate
|
||||
Etag:
|
||||
- W/"c5c1d51b68b499d00936c9eb1e8bfdbb"
|
||||
Referrer-Policy:
|
||||
- strict-origin-when-cross-origin
|
||||
Rndr-Id:
|
||||
- 3abc1256-5517-44a7
|
||||
Strict-Transport-Security:
|
||||
- max-age=63072000; includeSubDomains
|
||||
Vary:
|
||||
- Accept-Encoding
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Permitted-Cross-Domain-Policies:
|
||||
- none
|
||||
X-Render-Origin-Server:
|
||||
- Render
|
||||
X-Request-Id:
|
||||
- aaf85301-dd16-4b9b-a3a4-c4fbcf1d3f55
|
||||
X-Runtime:
|
||||
- '0.014386'
|
||||
X-Xss-Protection:
|
||||
- '0'
|
||||
Cf-Cache-Status:
|
||||
- DYNAMIC
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=OaVSdNPSl6CQ8gbhnDzkCisX2ILOEWAwweMW3rXXP5rBKuxZoDT024srQWmHKGLsCEhpt4G9mqCthDwlHu2%2BuZ3AyTJQcnBONtE%2FNQ7fKT9x8nLz4mnqL8iyynLuRWQSUJ8SWMj5"}],"group":"cf-nel","max_age":604800}'
|
||||
Nel:
|
||||
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
|
||||
Speculation-Rules:
|
||||
- '"/cdn-cgi/speculation"'
|
||||
Server:
|
||||
- cloudflare
|
||||
Cf-Ray:
|
||||
- 940b109d086eb4b8-ORD
|
||||
Alt-Svc:
|
||||
- h3=":443"; ma=86400
|
||||
Server-Timing:
|
||||
- cfL4;desc="?proto=TCP&rtt=32457&min_rtt=26792&rtt_var=14094&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2826&recv_bytes=878&delivery_rate=108091&cwnd=229&unsent_bytes=0&cid=a6f330e4d5f16682&ts=309&x=0"
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"id":"user_3208c49393f54b3e974795e4bea5b864","email":"zach@maybe.co","name":"Zach
|
||||
Gollwitzer","plan":"Business","api_calls_remaining":249733,"api_limit":250000,"credits_reset_at":"2025-06-01T00:00:00.000-04:00","current_period_start":"2025-05-01T00:00:00.000-04:00"}'
|
||||
recorded_at: Fri, 16 May 2025 13:01:39 GMT
|
||||
recorded_with: VCR 6.3.1
|
||||
@@ -1,105 +0,0 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://api.synthfinance.com/tickers/AAPL?operating_mic=XNAS
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ''
|
||||
headers:
|
||||
Authorization:
|
||||
- Bearer <SYNTH_API_KEY>
|
||||
X-Source:
|
||||
- maybe_app
|
||||
X-Source-Type:
|
||||
- managed
|
||||
User-Agent:
|
||||
- Faraday v2.13.1
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Date:
|
||||
- Fri, 16 May 2025 13:01:37 GMT
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Connection:
|
||||
- keep-alive
|
||||
Cache-Control:
|
||||
- max-age=0, private, must-revalidate
|
||||
Etag:
|
||||
- W/"75f336ad88e262c72044e8b865265298"
|
||||
Referrer-Policy:
|
||||
- strict-origin-when-cross-origin
|
||||
Rndr-Id:
|
||||
- ba973abf-7d96-4a9a
|
||||
Strict-Transport-Security:
|
||||
- max-age=63072000; includeSubDomains
|
||||
Vary:
|
||||
- Accept-Encoding
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Permitted-Cross-Domain-Policies:
|
||||
- none
|
||||
X-Render-Origin-Server:
|
||||
- Render
|
||||
X-Request-Id:
|
||||
- 76cb13a6-0d7e-4c36-8df9-bb63110d9e2a
|
||||
X-Runtime:
|
||||
- '0.099716'
|
||||
X-Xss-Protection:
|
||||
- '0'
|
||||
Cf-Cache-Status:
|
||||
- DYNAMIC
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=aDn7ApAO9Ma86gZ%2BJKCUCFjH2Re%2BtXdB5gcqYj2KTGXJKNpgf5TNgzbrp5%2Bw%2FGL5nTvtp%2B7cxT8MMcLWjAV6Ne1r6z5YBFq1K4W7Zw5m1lhMiqYLnTnEs2Oq85TjzOvpsE%2BmC33d"}],"group":"cf-nel","max_age":604800}'
|
||||
Nel:
|
||||
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
|
||||
Speculation-Rules:
|
||||
- '"/cdn-cgi/speculation"'
|
||||
Server:
|
||||
- cloudflare
|
||||
Cf-Ray:
|
||||
- 940b10910abdd2ec-ORD
|
||||
Alt-Svc:
|
||||
- h3=":443"; ma=86400
|
||||
Server-Timing:
|
||||
- cfL4;desc="?proto=TCP&rtt=28163&min_rtt=27237&rtt_var=12066&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2827&recv_bytes=905&delivery_rate=83590&cwnd=239&unsent_bytes=0&cid=7ef62bd693b52ccd&ts=240&x=0"
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"data":{"ticker":"AAPL","name":"Apple Inc.","links":{"homepage_url":"https://www.apple.com"},"logo_url":"https://logo.synthfinance.com/ticker/AAPL","description":"Apple
|
||||
Inc. designs, manufactures, and markets smartphones, personal computers, tablets,
|
||||
wearables, and accessories worldwide. The company offers iPhone, a line of
|
||||
smartphones; Mac, a line of personal computers; iPad, a line of multi-purpose
|
||||
tablets; and wearables, home, and accessories comprising AirPods, Apple TV,
|
||||
Apple Watch, Beats products, and HomePod. It also provides AppleCare support
|
||||
and cloud services; and operates various platforms, including the App Store
|
||||
that allow customers to discover and download applications and digital content,
|
||||
such as books, music, video, games, and podcasts. In addition, the company
|
||||
offers various services, such as Apple Arcade, a game subscription service;
|
||||
Apple Fitness+, a personalized fitness service; Apple Music, which offers
|
||||
users a curated listening experience with on-demand radio stations; Apple
|
||||
News+, a subscription news and magazine service; Apple TV+, which offers exclusive
|
||||
original content; Apple Card, a co-branded credit card; and Apple Pay, a cashless
|
||||
payment service, as well as licenses its intellectual property. The company
|
||||
serves consumers, and small and mid-sized businesses; and the education, enterprise,
|
||||
and government markets. It distributes third-party applications for its products
|
||||
through the App Store. The company also sells its products through its retail
|
||||
and online stores, and direct sales force; and third-party cellular network
|
||||
carriers, wholesalers, retailers, and resellers. Apple Inc. was founded in
|
||||
1976 and is headquartered in Cupertino, California.","kind":"common stock","cik":"0000320193","currency":"USD","address":{"country":"USA","address_line1":"One
|
||||
Apple Park Way","city":"Cupertino","state":"CA","postal_code":"95014"},"exchange":{"name":"Nasdaq/Ngs
|
||||
(Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
|
||||
States","country_code":"US","timezone":"America/New_York"},"ceo":"Mr. Timothy
|
||||
D. Cook","founding_year":1976,"industry":"Consumer Electronics","sector":"Technology","phone":"408-996-1010","total_employees":161000,"composite_figi":"BBG000B9Y5X2","market_data":{"high_today":212.96,"low_today":209.54,"open_today":210.95,"close_today":211.45,"volume_today":44979900.0,"fifty_two_week_high":260.1,"fifty_two_week_low":169.21,"average_volume":61769396.875,"price_change":0.0,"percent_change":0.0}},"meta":{"credits_used":1,"credits_remaining":249737}}'
|
||||
recorded_at: Fri, 16 May 2025 13:01:37 GMT
|
||||
recorded_with: VCR 6.3.1
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://api.synthfinance.com/tickers/AAPL/open-close?end_date=2024-08-01&operating_mic_code=XNAS&page=1&start_date=2024-08-01
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ''
|
||||
headers:
|
||||
Authorization:
|
||||
- Bearer <SYNTH_API_KEY>
|
||||
X-Source:
|
||||
- maybe_app
|
||||
X-Source-Type:
|
||||
- managed
|
||||
User-Agent:
|
||||
- Faraday v2.13.1
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Date:
|
||||
- Fri, 16 May 2025 13:01:36 GMT
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Connection:
|
||||
- keep-alive
|
||||
Cache-Control:
|
||||
- max-age=0, private, must-revalidate
|
||||
Etag:
|
||||
- W/"72340d82266397447b865407dda15492"
|
||||
Referrer-Policy:
|
||||
- strict-origin-when-cross-origin
|
||||
Rndr-Id:
|
||||
- 4c3462aa-2471-40b4
|
||||
Strict-Transport-Security:
|
||||
- max-age=63072000; includeSubDomains
|
||||
Vary:
|
||||
- Accept-Encoding
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Permitted-Cross-Domain-Policies:
|
||||
- none
|
||||
X-Render-Origin-Server:
|
||||
- Render
|
||||
X-Request-Id:
|
||||
- bdbc757d-2528-44c3-ae08-9788e8ee15f7
|
||||
X-Runtime:
|
||||
- '0.034898'
|
||||
X-Xss-Protection:
|
||||
- '0'
|
||||
Cf-Cache-Status:
|
||||
- DYNAMIC
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=2Mu4PK4XTsAq%2Bn1%2F2yxy%2Blj7kz3ZCiQ9t8ikr2m19BrhQhrqfeUQfPwxbLc1WIgGMIxpPInKYtDVIX3En%2FGpTNQLAeu%2FpuLKv%2BRmCx%2B7u28od5L%2F9%2BLmEhFWqJjs8Y6C1O2a3SKv"}],"group":"cf-nel","max_age":604800}'
|
||||
Nel:
|
||||
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
|
||||
Speculation-Rules:
|
||||
- '"/cdn-cgi/speculation"'
|
||||
Server:
|
||||
- cloudflare
|
||||
Cf-Ray:
|
||||
- 940b108f29129d03-ORD
|
||||
Alt-Svc:
|
||||
- h3=":443"; ma=86400
|
||||
Server-Timing:
|
||||
- cfL4;desc="?proto=TCP&rtt=27793&min_rtt=26182&rtt_var=13041&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2827&recv_bytes=970&delivery_rate=74111&cwnd=244&unsent_bytes=0&cid=9bcc030369a615fb&ts=210&x=0"
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"ticker":"AAPL","currency":"USD","exchange":{"name":"Nasdaq/Ngs (Global
|
||||
Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
|
||||
States","country_code":"US","timezone":"America/New_York"},"prices":[{"date":"2024-08-01","open":224.37,"close":218.36,"high":224.48,"low":217.02,"volume":62501000}],"paging":{"prev":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=\u0026start_date=2024-08-01","next":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=\u0026start_date=2024-08-01","total_records":1,"current_page":1,"per_page":100,"total_pages":1},"meta":{"total_records":1,"credits_used":1,"credits_remaining":249738}}'
|
||||
recorded_at: Fri, 16 May 2025 13:01:36 GMT
|
||||
recorded_with: VCR 6.3.1
|
||||
File diff suppressed because one or more lines are too long
@@ -1,104 +0,0 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://api.synthfinance.com/tickers/search?country_code=US&dataset=limited&limit=25&name=AAPL
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ''
|
||||
headers:
|
||||
Authorization:
|
||||
- Bearer <SYNTH_API_KEY>
|
||||
X-Source:
|
||||
- maybe_app
|
||||
X-Source-Type:
|
||||
- managed
|
||||
User-Agent:
|
||||
- Faraday v2.13.1
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Date:
|
||||
- Fri, 16 May 2025 13:01:38 GMT
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Connection:
|
||||
- keep-alive
|
||||
Cache-Control:
|
||||
- max-age=0, private, must-revalidate
|
||||
Etag:
|
||||
- W/"3e444869eacbaf17006766a691cc8fdc"
|
||||
Referrer-Policy:
|
||||
- strict-origin-when-cross-origin
|
||||
Rndr-Id:
|
||||
- 701ae22a-18c8-4e62
|
||||
Strict-Transport-Security:
|
||||
- max-age=63072000; includeSubDomains
|
||||
Vary:
|
||||
- Accept-Encoding
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Permitted-Cross-Domain-Policies:
|
||||
- none
|
||||
X-Render-Origin-Server:
|
||||
- Render
|
||||
X-Request-Id:
|
||||
- edb55bc6-e3ea-470b-b7af-9b4d9883420b
|
||||
X-Runtime:
|
||||
- '0.355152'
|
||||
X-Xss-Protection:
|
||||
- '0'
|
||||
Cf-Cache-Status:
|
||||
- DYNAMIC
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=QGeBWdYED%2F%2FgT9BzborFAnM%2FG6UiNmI0ej212XGHWdFwYXUvTJ2GyqA9hMJrpYIvgbHdQ9Ed0MsQUv3KFb57VXQq0T6UXTNPa%2BFRPepK0hsXeGDLxch04v6KnkTATqcw2M8HuYHS"}],"group":"cf-nel","max_age":604800}'
|
||||
Nel:
|
||||
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
|
||||
Speculation-Rules:
|
||||
- '"/cdn-cgi/speculation"'
|
||||
Server:
|
||||
- cloudflare
|
||||
Cf-Ray:
|
||||
- 940b1097a830f856-ORD
|
||||
Alt-Svc:
|
||||
- h3=":443"; ma=86400
|
||||
Server-Timing:
|
||||
- cfL4;desc="?proto=TCP&rtt=26401&min_rtt=25556&rtt_var=11273&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2825&recv_bytes=939&delivery_rate=89615&cwnd=244&unsent_bytes=0&cid=cf6d0758d165295d&ts=500&x=0"
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"data":[{"symbol":"AAPL","name":"Apple Inc.","logo_url":"https://logo.synthfinance.com/ticker/AAPL","currency":"USD","exchange":{"name":"Nasdaq/Ngs
|
||||
(Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
|
||||
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"APLY","isin":"US88634T8577","name":"YieldMax
|
||||
AAPL Option Income ETF","logo_url":"https://logo.synthfinance.com/ticker/APLY","currency":"USD","exchange":{"name":"Nyse
|
||||
Arca","mic_code":"ARCX","operating_mic_code":"XNYS","acronym":"NYSE","country":"United
|
||||
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPD","name":"Direxion
|
||||
Daily AAPL Bear 1X ETF","logo_url":"https://logo.synthfinance.com/ticker/AAPD","currency":"USD","exchange":{"name":"Nasdaq/Nms
|
||||
(Global Market)","mic_code":"XNMS","operating_mic_code":"XNAS","acronym":"","country":"United
|
||||
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPU","isin":"US25461A8743","name":"Direxion
|
||||
Daily AAPL Bull 2X Shares","logo_url":"https://logo.synthfinance.com/ticker/AAPU","currency":"USD","exchange":{"name":"Nasdaq/Nms
|
||||
(Global Market)","mic_code":"XNMS","operating_mic_code":"XNAS","acronym":"","country":"United
|
||||
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPB","isin":"XXXXXXXR8842","name":"GraniteShares
|
||||
2x Long AAPL Daily ETF","logo_url":"https://logo.synthfinance.com/ticker/AAPB","currency":"USD","exchange":{"name":"Nasdaq/Ngs
|
||||
(Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
|
||||
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPD","isin":"US25461A3041","name":"Direxion
|
||||
Daily AAPL Bear 1X Shares","logo_url":"https://logo.synthfinance.com/ticker/AAPD","currency":"USD","exchange":{"name":"Nasdaq/Ngs
|
||||
(Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
|
||||
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPU","isin":"US25461A8743","name":"Direxion
|
||||
Daily AAPL Bull 1.5X Shares","logo_url":"https://logo.synthfinance.com/ticker/AAPU","currency":"USD","exchange":{"name":"Nasdaq/Ngs
|
||||
(Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
|
||||
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPJ","isin":"US00037T1034","name":"AAP,
|
||||
Inc.","logo_url":"https://logo.synthfinance.com/ticker/AAPJ","currency":"USD","exchange":{"name":"Otc
|
||||
Pink Marketplace","mic_code":"PINX","operating_mic_code":"OTCM","acronym":"","country":"United
|
||||
States","country_code":"US","timezone":"America/New_York"}}]}'
|
||||
recorded_at: Fri, 16 May 2025 13:01:38 GMT
|
||||
recorded_with: VCR 6.3.1
|
||||
@@ -1,82 +0,0 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://api.synthfinance.com/user
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ''
|
||||
headers:
|
||||
Authorization:
|
||||
- Bearer <SYNTH_API_KEY>
|
||||
X-Source:
|
||||
- maybe_app
|
||||
X-Source-Type:
|
||||
- managed
|
||||
User-Agent:
|
||||
- Faraday v2.13.1
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Date:
|
||||
- Fri, 16 May 2025 13:01:36 GMT
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Connection:
|
||||
- keep-alive
|
||||
Cache-Control:
|
||||
- max-age=0, private, must-revalidate
|
||||
Etag:
|
||||
- W/"7b8c2bf0cba54bc26b78bdc6e611dcbd"
|
||||
Referrer-Policy:
|
||||
- strict-origin-when-cross-origin
|
||||
Rndr-Id:
|
||||
- 1b53adf6-b391-45b2
|
||||
Strict-Transport-Security:
|
||||
- max-age=63072000; includeSubDomains
|
||||
Vary:
|
||||
- Accept-Encoding
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Permitted-Cross-Domain-Policies:
|
||||
- none
|
||||
X-Render-Origin-Server:
|
||||
- Render
|
||||
X-Request-Id:
|
||||
- f88670a2-81d2-48b6-8d73-a911c846e330
|
||||
X-Runtime:
|
||||
- '0.018749'
|
||||
X-Xss-Protection:
|
||||
- '0'
|
||||
Cf-Cache-Status:
|
||||
- DYNAMIC
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=oH4OsWB6itK0jpi%2FPs%2BswVyCZIbkJGPfyJaoR4TKFtTAfmnqa8Lp6aZhv22WKzotXJuAKbh99VdYdZIOkeIPWbYTc6j4rGw%2BkQB3Hw%2Fc44QxDBJFdIo6wJNe8TGiPAZ%2BvgoBVHWn"}],"group":"cf-nel","max_age":604800}'
|
||||
Nel:
|
||||
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
|
||||
Speculation-Rules:
|
||||
- '"/cdn-cgi/speculation"'
|
||||
Server:
|
||||
- cloudflare
|
||||
Cf-Ray:
|
||||
- 940b108c38f66392-ORD
|
||||
Alt-Svc:
|
||||
- h3=":443"; ma=86400
|
||||
Server-Timing:
|
||||
- cfL4;desc="?proto=TCP&rtt=33369&min_rtt=25798&rtt_var=15082&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2826&recv_bytes=878&delivery_rate=112256&cwnd=205&unsent_bytes=0&cid=1b13324eb0768fd3&ts=285&x=0"
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"id":"user_3208c49393f54b3e974795e4bea5b864","email":"zach@maybe.co","name":"Zach
|
||||
Gollwitzer","plan":"Business","api_calls_remaining":249738,"api_limit":250000,"credits_reset_at":"2025-06-01T00:00:00.000-04:00","current_period_start":"2025-05-01T00:00:00.000-04:00"}'
|
||||
recorded_at: Fri, 16 May 2025 13:01:36 GMT
|
||||
recorded_with: VCR 6.3.1
|
||||
Reference in New Issue
Block a user