Files
sure/app/models/binance_account/holdings_processor.rb
Louis 455c74dcfa Add Binance support, heavily inspired by the Coinbase one (#1317)
* feat: add Binance support (Items, Accounts, Importers, Processor, and Sync)

* refactor: deduplicate 'stablecoins' constant and push stale_rate filter to SQL

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-04-07 14:43:17 +02:00

113 lines
3.5 KiB
Ruby

# frozen_string_literal: true
# Creates/updates Holdings for each asset in the combined BinanceAccount.
# One Holding per (symbol, source) pair.
class BinanceAccount::HoldingsProcessor
include BinanceAccount::UsdConverter
def initialize(binance_account)
@binance_account = binance_account
end
def process
unless account&.accountable_type == "Crypto"
Rails.logger.info "BinanceAccount::HoldingsProcessor - skipping: not a Crypto account"
return
end
assets = raw_assets
if assets.empty?
Rails.logger.info "BinanceAccount::HoldingsProcessor - no assets in payload"
return
end
assets.each { |asset| process_asset(asset) }
rescue StandardError => e
Rails.logger.error "BinanceAccount::HoldingsProcessor - error: #{e.message}"
nil
end
private
attr_reader :binance_account
def target_currency
binance_account.binance_item.family.currency
end
def account
binance_account.current_account
end
def raw_assets
binance_account.raw_payload&.dig("assets") || []
end
def process_asset(asset)
symbol = asset["symbol"] || asset[:symbol]
return if symbol.blank?
total = (asset["total"] || asset[:total]).to_d
source = asset["source"] || asset[:source]
return if total.zero?
ticker = symbol.include?(":") ? symbol : "CRYPTO:#{symbol}"
security = resolve_security(ticker, symbol)
return unless security
price_usd = fetch_price(symbol)
return if price_usd.nil?
amount_usd = total * price_usd
# Stale rate metadata is intentionally discarded here — it is captured and
# surfaced at the account level by BinanceAccount::Processor#process_account!.
amount, _stale, _rate_date = convert_from_usd(amount_usd, date: Date.current)
# Also convert per-unit price to target currency
price, _, _ = convert_from_usd(price_usd, date: Date.current)
import_adapter.import_holding(
security: security,
quantity: total,
amount: amount,
currency: target_currency,
date: Date.current,
price: price,
cost_basis: nil,
external_id: "binance_#{symbol}_#{source}_#{Date.current}",
account_provider_id: binance_account.account_provider&.id,
source: "binance",
delete_future_holdings: false
)
Rails.logger.info "BinanceAccount::HoldingsProcessor - imported #{total} #{symbol} (#{source}) @ #{price_usd} USD → #{amount} #{target_currency}"
rescue StandardError => e
Rails.logger.error "BinanceAccount::HoldingsProcessor - failed asset #{asset}: #{e.message}"
end
def import_adapter
@import_adapter ||= Account::ProviderImportAdapter.new(account)
end
def resolve_security(ticker, symbol)
BinanceAccount::SecurityResolver.resolve(ticker, symbol)
end
def fetch_price(symbol)
return 1.0 if BinanceAccount::STABLECOINS.include?(symbol)
provider = binance_account.binance_item&.binance_provider
return nil unless provider
%w[USDT BUSD FDUSD].each do |quote|
price_str = provider.get_spot_price("#{symbol}#{quote}")
return price_str.to_d if price_str.present?
end
Rails.logger.warn "BinanceAccount::HoldingsProcessor - no price found for #{symbol} across all quote pairs; skipping holding"
nil
end
end