{"record":{"id":"23192c59c8aebef2","repo":"we-promise/sure","slug":"no-price-found-for-symbol-on-date-23192c","errorCode":null,"errorMessage":"No price found for #{symbol} on #{date}","messagePattern":"No price found for #(.+?) on #(.+?)","errorType":"exception","errorClass":"Provider::MoexPublic::InvalidSecurityPriceError","httpStatus":null,"severity":"error","filePath":"app/models/provider/moex_public.rb","lineNumber":138,"sourceCode":"        logo_url: nil,\n        description: nil,\n        kind: instrument[:kind],\n        exchange_operating_mic: MOEX_MIC\n      )\n    end\n  end\n\n  def fetch_security_price(symbol:, exchange_operating_mic:, date:)\n    with_provider_response do\n      historical = fetch_security_prices(\n        symbol: symbol,\n        exchange_operating_mic: exchange_operating_mic,\n        start_date: date,\n        end_date: date\n      )\n\n      raise historical.error if historical.error.present?\n      raise InvalidSecurityPriceError, \"No price found for #{symbol} on #{date}\" if historical.data.blank?\n\n      # Exact date if present, else the nearest available close on or before it.\n      historical.data.find { |p| p.date == date } ||\n        historical.data.select { |p| p.date <= date }.max_by(&:date) ||\n        historical.data.first\n    end\n  end\n\n  def fetch_security_prices(symbol:, exchange_operating_mic:, start_date:, end_date:)\n    with_provider_response do\n      secid = normalize_secid(symbol)\n      instrument = resolve_instrument(secid)\n      bond = instrument[:market].to_s.downcase == \"bonds\"\n\n      prices = history_prices(secid, instrument, start_date, end_date, bond)\n\n      # The history endpoint does not carry the live/most-recent session, so for\n      # a range reaching today append the current marketdata price.","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/moex_public.rb#L120-L156","documentation":"Raised by Provider::MoexPublic#fetch_security_price (an InvalidSecurityPriceError) when a same-day history query (start_date == end_date == date) for a resolved SECID returns zero candles. MOEX ISS /history/... only returns rows for trading sessions, so the single-day window is empty whenever `date` is not a session day or the board had no trades. Resolution already succeeded (resolve_instrument ran), so this is purely 'no rows in the candle history for that exact day'.","triggerScenarios":"Calling fetch_security_price with a Saturday, Sunday, or Russian public holiday as date; a thinly-traded bond board with no trades that session; a date after delisting; a date before the instrument listed on the resolved board; requesting a future date.","commonSituations":"Daily portfolio valuation jobs that run on weekends and naively ask for today's price; syncing US-style date assumptions against the MOEX calendar (Russia has its own holiday set); illiquid corporate bonds on non-primary boards.","solutions":["Widen the window — call fetch_security_prices(symbol:, start_date: date - 7.days, end_date: date) and pick select { |p| p.date <= date }.max_by(&:date), exactly like the MFAPI provider does.","Skip non-trading days: check the date against the MOEX trading calendar (or fall back to 'most recent session' via the provider's current-price path) before requesting.","Validate the date is within the instrument's listing period (from /securities/{secid}.json metadata) and not in the future.","Catch InvalidSecurityPriceError per-date and carry forward the last known price instead of failing the valuation batch."],"exampleFix":"# before\nhistorical = fetch_security_prices(symbol: symbol, exchange_operating_mic: exchange_operating_mic, start_date: date, end_date: date)\n\n# after\nhistorical = fetch_security_prices(symbol: symbol, exchange_operating_mic: exchange_operating_mic, start_date: date - 7.days, end_date: date)\n# raise only if the whole 7-day window is empty, then reuse the existing closest-previous fallback below","handlingStrategy":"validation","validationCode":"window_start = date - 7.days # ask for a window, not one day, so non-trading dates still resolve\nprices = provider.fetch_security_prices(symbol: secid, exchange_operating_mic: mic, start_date: window_start, end_date: date)","typeGuard":null,"tryCatchPattern":"begin\n  price = provider.fetch_security_price(symbol: secid, exchange_operating_mic: mic, date: date)\nrescue Provider::MoexPublic::InvalidSecurityPriceError\n  price = nil # carry forward previous close\nend","preventionTips":["Never request a single trading day from a session-based exchange; always pass a lookback window.","Skip known non-trading days (weekends, exchange holidays) in valuation jobs.","Cache the last good price per security so empty days degrade silently."],"tags":["moex","iss-api","historical-prices","trading-calendar","empty-data"],"backgroundTag":"no-historical-price-data","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}