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

  1. Retry later -- missing metadata for new issues usually fills in; the instrument cache TTL will refresh BondBy data
  2. Skip the bond from pricing and flag it (treat as provider-data gap) rather than retrying every sync
  3. 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

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


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/17a12d0fd2a66366. Report an issue: GitHub.