we-promise/sure · error · Provider::TinkoffInvest::InvalidSecurityPriceError
Missing or invalid T-Invest bond nominal for #{symbol}
Error message
Missing or invalid T-Invest bond nominal for #{symbol} What it means
For bonds, T-Invest quotes prices as percent of par, so the client multiplies by the nominal fetched from InstrumentsService/BondBy (quotation_to_d on ins['nominal']). If BondBy returns no nominal or a non-positive value, building a money price is impossible; the code raises InvalidSecurityPriceError instead of producing a wrong (zero/par-less) price, per the comment: a missing nominal is a provider-data failure, not a zero price. The amortization flag from the same call only affects whether history candles are used.
Source
Thrown at app/models/provider/tinkoff_invest.rb:150
def fetch_security_prices(symbol:, exchange_operating_mic:, start_date:, end_date:)
with_provider_response do
short = resolve_short(symbol, exchange_operating_mic)
raise Error, "Unknown T-Invest instrument: #{symbol}" if short.nil?
uid = short["uid"]
bond = short["instrumentType"].to_s == "bond"
currency = short["currency"].to_s.upcase
mic = mic_for(short["classCode"], short["exchange"])
# Bonds quote in % of par; multiply by nominal to get a money price. A
# missing/invalid nominal is a provider-data failure, not a zero price.
nominal = nil
amortizing = false
if bond
info = bond_info(uid)
nominal = info[:nominal]
amortizing = info[:amortizing]
raise InvalidSecurityPriceError, "Missing or invalid T-Invest bond nominal for #{symbol}" if nominal.nil? || nominal <= 0
end
ticker = short["ticker"].to_s
build = ->(date, raw) { Price.new(symbol: ticker, date: date, price: (bond ? (raw / 100) * nominal : raw), currency: currency, exchange_operating_mic: mic) }
# BondBy returns only the CURRENT nominal. For an amortizing bond the par
# shrinks over time, so applying today's nominal to historical percent-of-
# par closes would underprice them — skip the candle history and return
# just the live price. Fixed-par bonds and equities use full history.
prices = (bond && amortizing) ? [] : candle_closes(uid, start_date, end_date).map { |date, close| build.call(date, close) }
# The candle endpoint lags the live session; append the last price for a
# range reaching today.
if end_date >= Date.current
last = last_price(uid)
if last
prices.reject! { |p| p.date == Date.current }
prices << build.call(Date.current, last)View on GitHub (pinned to e69894adb9)
Solutions
- Retry later -- missing metadata for new issues usually fills in; the instrument cache TTL will refresh BondBy data
- Skip the bond from pricing and flag it (treat as provider-data gap) rather than retrying every sync
- If it affects many bonds, capture the raw BondBy body in a debug log and check whether T-Invest renamed/omitted 'nominal' (e.g. use GetInstrumentBy fields as a fallback source for par)
Example fix
# before
begin
price = provider.fetch_security_price(symbol: isin, exchange_operating_mic: mic, date: date)
rescue => e
raise
end
# after
begin
price = provider.fetch_security_price(symbol: isin, exchange_operating_mic: mic, date: date)
rescue Provider::TinkoffInvest::InvalidSecurityPriceError => e
security.update!(price_sync_failed_at: Time.current, price_sync_error: e.message)
Rails.logger.warn("Skipping bond pricing, provider data gap: #{e.message}")
end Defensive patterns
Strategy: try-catch
Try / catch
begin price = provider.fetch_security_price(symbol:, exchange_operating_mic:, date:) rescue Provider::TinkoffInvest::InvalidSecurityPriceError => e security.update!(price_sync_error: e.message) # provider-data gap for this bond; skip, don't retry-loop end
Prevention
- Treat bond-nominal failures as data gaps: back off (hours/days), not seconds
- Alert when the same bond fails repeatedly so support can escalate missing metadata to the provider
When it happens
Trigger: fetch_security_prices/fetch_security_price for a bond whose BondBy payload lacks 'nominal' (null/zero/negative quotation) -- e.g. a newly issued bond before metadata is populated, an odd lot/off-board listing, or an upstream schema gap for certain bond types (e.g. OFZ with unusual par handling).
Common situations: New bond issues synced before T-Invest populates BondBy metadata; regional/corporate bonds with sparse metadata; upstream API change renaming the nominal field so quotation_to_d gets nil.
Related errors
- No T-Invest price for #{symbol} on #{date}
- Unknown T-Invest instrument: #{symbol}
- Unexpected response format from search endpoint
- No prices found for security #{symbol} on date #{date}
- API error: #{error_message}
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/17a12d0fd2a66366.
Report an issue: GitHub.