mirror of
https://github.com/we-promise/sure.git
synced 2026-07-19 16:25:24 +00:00
Adds Provider::TinkoffInvest, a token-based securities provider built on the public T-Invest REST gateway (invest-public-api.tinkoff.ru/rest). It serves prices for Russian instruments (shares, ETF/БПИФ, bonds) and, crucially, brand logos via the T-Invest CDN — the authoritative logo source for MOEX instruments, which ISS (MoexPublic) does not provide. - Registry: register `tinkoff_invest` under the :securities concept; token via ENV TINKOFF_INVEST_API_KEY or encrypted Setting.tinkoff_invest_api_key. - Logos independent of the price provider: Security#import_brand_logo consults T-Invest for a logo whenever a token is configured (after the price-provider metadata fetch, so it never short-circuits website_url backfill). Gated on token presence, not the securities checklist. - display_logo_url: with no website domain, a stored provider logo (T-Invest) now beats the ticker-only Brandfetch lettermark; when a domain exists, Brandfetch still wins (unchanged). - MoexPublic no longer reports moex.com as the issuer website — it's the exchange, not the issuer, and would make Brandfetch render the exchange logo for every instrument and shadow the real brand logo. - Prices: GetCandles (daily, paged) + GetLastPrices; Quotation units+nano/1e9; bonds priced as percent-of-par x nominal (missing nominal raises, not 0). - Settings: encrypted token field (always shown) + provider checkbox + en locale. - Tests for search/info/logo-url/prices/bond/incomplete-candle and display logic. Co-authored-by: Claude <noreply@anthropic.com>
195 lines
5.2 KiB
Ruby
195 lines
5.2 KiB
Ruby
class Provider::Registry
|
|
include ActiveModel::Validations
|
|
|
|
Error = Class.new(StandardError)
|
|
|
|
CONCEPTS = %i[exchange_rates securities llm]
|
|
|
|
validates :concept, inclusion: { in: CONCEPTS }
|
|
|
|
class << self
|
|
def for_concept(concept)
|
|
new(concept.to_sym)
|
|
end
|
|
|
|
def get_provider(name)
|
|
send(name)
|
|
rescue NoMethodError
|
|
raise Error.new("Provider '#{name}' not found in registry")
|
|
end
|
|
|
|
# Resolves the LLM provider for batch/PDF flows, honoring Setting.llm_provider
|
|
# the way chat does: prefer the configured provider, but fall back to whichever
|
|
# one actually has credentials so an install that swaps providers (or has only
|
|
# one configured) keeps working. Returns nil when neither is configured —
|
|
# callers guard on that.
|
|
def preferred_llm_provider
|
|
order = Setting.llm_provider == "anthropic" ? %i[anthropic openai] : %i[openai anthropic]
|
|
order.each do |name|
|
|
provider = get_provider(name)
|
|
return provider if provider
|
|
end
|
|
nil
|
|
end
|
|
|
|
def plaid_provider_for_region(region)
|
|
region.to_sym == :us ? plaid_us : plaid_eu
|
|
end
|
|
|
|
private
|
|
def stripe
|
|
secret_key = ENV["STRIPE_SECRET_KEY"]
|
|
webhook_secret = ENV["STRIPE_WEBHOOK_SECRET"]
|
|
|
|
return nil unless secret_key.present? && webhook_secret.present?
|
|
|
|
Provider::Stripe.new(secret_key:, webhook_secret:)
|
|
end
|
|
|
|
def twelve_data
|
|
api_key = ENV["TWELVE_DATA_API_KEY"].presence || Setting.twelve_data_api_key
|
|
|
|
return nil unless api_key.present?
|
|
|
|
Provider::TwelveData.new(api_key)
|
|
end
|
|
|
|
def plaid_us
|
|
Provider::PlaidAdapter.ensure_configuration_loaded
|
|
config = Rails.application.config.plaid
|
|
|
|
return nil unless config.present?
|
|
|
|
Provider::Plaid.new(config, region: :us)
|
|
end
|
|
|
|
def plaid_eu
|
|
Provider::PlaidEuAdapter.ensure_configuration_loaded
|
|
config = Rails.application.config.plaid_eu
|
|
|
|
return nil unless config.present?
|
|
|
|
Provider::Plaid.new(config, region: :eu)
|
|
end
|
|
|
|
def github
|
|
Provider::Github.new
|
|
end
|
|
|
|
def openai
|
|
access_token = ENV["OPENAI_ACCESS_TOKEN"].presence || Setting.openai_access_token
|
|
|
|
return nil unless access_token.present?
|
|
|
|
uri_base = ENV["OPENAI_URI_BASE"].presence || Setting.openai_uri_base
|
|
model = ENV["OPENAI_MODEL"].presence || Setting.openai_model
|
|
|
|
if uri_base.present? && model.blank?
|
|
Rails.logger.error("Custom OpenAI provider configured without a model; please set OPENAI_MODEL or Setting.openai_model")
|
|
return nil
|
|
end
|
|
|
|
Provider::Openai.new(access_token, uri_base: uri_base, model: model)
|
|
end
|
|
|
|
def anthropic
|
|
access_token = ENV["ANTHROPIC_ACCESS_TOKEN"].presence ||
|
|
ENV["ANTHROPIC_API_KEY"].presence ||
|
|
Setting.anthropic_access_token
|
|
|
|
return nil unless access_token.present?
|
|
|
|
base_url = ENV["ANTHROPIC_BASE_URL"].presence || Setting.anthropic_base_url
|
|
model = ENV["ANTHROPIC_MODEL"].presence || Setting.anthropic_model
|
|
|
|
Provider::Anthropic.new(access_token, base_url: base_url, model: model)
|
|
end
|
|
|
|
def yahoo_finance
|
|
Provider::YahooFinance.new
|
|
end
|
|
|
|
def tiingo
|
|
api_key = ENV["TIINGO_API_KEY"].presence || Setting.tiingo_api_key # pipelock:ignore
|
|
|
|
return nil unless api_key.present?
|
|
|
|
Provider::Tiingo.new(api_key)
|
|
end
|
|
|
|
def eodhd
|
|
api_key = ENV["EODHD_API_KEY"].presence || Setting.eodhd_api_key # pipelock:ignore
|
|
|
|
return nil unless api_key.present?
|
|
|
|
Provider::Eodhd.new(api_key)
|
|
end
|
|
|
|
def alpha_vantage
|
|
api_key = ENV["ALPHA_VANTAGE_API_KEY"].presence || Setting.alpha_vantage_api_key # pipelock:ignore
|
|
|
|
return nil unless api_key.present?
|
|
|
|
Provider::AlphaVantage.new(api_key)
|
|
end
|
|
|
|
def mfapi
|
|
Provider::Mfapi.new
|
|
end
|
|
|
|
def binance_public
|
|
Provider::BinancePublic.new
|
|
end
|
|
|
|
def moex_public
|
|
Provider::MoexPublic.new
|
|
end
|
|
|
|
def tinkoff_invest
|
|
api_key = ENV["TINKOFF_INVEST_API_KEY"].presence || Setting.tinkoff_invest_api_key # pipelock:ignore
|
|
|
|
return nil unless api_key.present?
|
|
|
|
Provider::TinkoffInvest.new(api_key)
|
|
end
|
|
end
|
|
|
|
def initialize(concept)
|
|
@concept = concept
|
|
validate!
|
|
end
|
|
|
|
def providers
|
|
available_providers.map { |p| self.class.send(p) }.compact
|
|
end
|
|
|
|
# Returns the list of provider key names (symbols) registered for this concept.
|
|
def provider_keys
|
|
available_providers
|
|
end
|
|
|
|
def get_provider(name)
|
|
provider_method = available_providers.find { |p| p == name.to_sym }
|
|
|
|
raise Error.new("Provider '#{name}' not found for concept: #{concept}") unless provider_method.present?
|
|
|
|
self.class.send(provider_method)
|
|
end
|
|
|
|
private
|
|
attr_reader :concept
|
|
|
|
def available_providers
|
|
case concept
|
|
when :exchange_rates
|
|
%i[twelve_data yahoo_finance moex_public]
|
|
when :securities
|
|
%i[twelve_data yahoo_finance tiingo eodhd alpha_vantage mfapi binance_public moex_public tinkoff_invest]
|
|
when :llm
|
|
%i[openai anthropic]
|
|
else
|
|
%i[plaid_us plaid_eu github openai anthropic]
|
|
end
|
|
end
|
|
end
|