Skip to main content
Parameters, response columns and types are generated from the SQL catalog and render under Endpoints → Securities lending in the sidebar. This page carries what a schema cannot: the conventions, the nulls that are deliberate, and the traps.

Overview

Four views over B3’s securities-lending market: what is on loan, at what rate, and who traded it. They are the closest thing the Brazilian market publishes to a short interest report. These are views: GET, PostgREST filters (?ticker=eq.PETR4), limit/offset paging. Investor-type flow is on Investor flow.
History starts when SILO started capturing, and can never be extended backwards. B3 keeps roughly 21 business days of these tables and publishes no archive. This is the only part of the warehouse that behaves this way.
  • A short window is the source’s retention, not a data problem. Do not read the first date as a market event, a listing date, or a bug.
  • No long-run average, 52-week range or year-over-year change until SILO has held that long. A ratio against a “historical norm” with three weeks behind it describes the capture window, not the market.
An over-wide request to B3 returns HTTP 200 with a silently clamped window, so every ingest reconciles the sessions it received against the ones it asked for.
Sorting on a nullable column: nulls sort last by default in PostgREST. pct_float and days_to_cover are null whenever their denominator is missing, so order=pct_float.desc buries those rows at the end rather than dropping them. Add .nullslast explicitly if you rely on that placement.

GET /short_interest

Short interest per (ticker, trade_date), built from B3’s own Total rows so balances are never double-counted. Filters: ticker, trade_date (sessions only, no weekend rows), float_basis (required for any ranking), instrument_category (SHARES, UNIT, ETF EQUITIES, BDR, FUNDS). An unfiltered days-to-cover screen is dominated by index ETFs, not the crowded single-name shorts the screen is for.
Read pct_float together with float_basis. Without it you get a confident, wrong ranking.They are never comparable. Any ranking, screen or cross-section must filter to one basis first; the free-float basis gives systematically larger percentages, so an unfiltered “most shorted” list is really a list of index constituents.float_denominator carries the number actually used. Index priority is IBRA → SMLL → IBXX → IBOV: weight caps can only shrink the quantity below the real float, so the broadest index is the closest published estimate.
Example, the most shorted index names in a session (the float_basis filter is not optional):
curl
Check coverage for the newest session before pinning a trade_date.

GET /short_interest_by_sector

One row per (sector, trade_date). Filters: trade_date, sector. short_value is the sector total; short_value_equities restricts it to SHARES and UNIT; tickers counts what went into the bucket. Note: Não classificado is a real bucket. Sector comes from the B3 index portfolios, so ETFs, BDRs and every off-index name land there (bucketed, not dropped). For single-name risk read short_value_equities; the unclassified bucket is almost entirely ETFs and BDRs.

GET /lending_trades

The session’s lending trades, aggregated per (ticker, trade_date). Note: B3’s own published average weights by the number of trades; rate_pct weights by quantity. Both are right. Measured 2026-09-10 across the 569 tickers both cover: mean absolute difference 0.037 percentage points, 561 of 569 within 0.5pp, largest gap 3.81pp.

GET /lending_participants

One row per (ticker, trade_date, broker_code): what each brokerage lent and borrowed.
doador and tomador are BROKERAGES, not beneficial owners. Reading them as owners produces a confident, wrong story about who is short.B3 names about 33 participants in a whole session, and on 2026-09-10 32,197 of 43,165 trades (74.6%) carried the same code on both legs: a broker crossing its own client book. A large quantity_borrowed through a broker is its clients’ position, not a view the broker took.internal_legs and internal_qty tell the two apart. A high internal share is client churn; a low one is flow that actually crossed the market. That session, J.P. Morgan sat at ~6% internal (directional) while Morgan Stanley was at ~78% (client churn).The split is published rather than netted away: dropping self-crossed trades makes the rest look like inter-broker conviction, keeping them makes churn look like directional demand. The choice is yours.
Python

Paging and empty results

These are views: the 1,000-row page is PostgREST’s db-max-rows and it cuts silently with a 200, keeping the oldest rows. It does not raise like the RPC functions.
  • Send Prefer: count=exact and read Content-Range: 0-999/* means the response was cut; 0-999/4382 gives the true total.
  • Page with limit / offset and an explicit order; without a total order a page boundary can duplicate one row and drop another.
  • The SDK: silo.short_interest(...) returns one page and raises SiloTruncated on an unbounded cut; silo.view_all("short_interest", order="trade_date.desc") walks every page.
  • A whole session of lending_participants is over one page (one row per (ticker, broker)): filter by ticker or page it.
Unknown ticker, or a known ticker with no rows in the window, is 200 [], never a fabricated balance. With ~21 business days of retention an empty window is common: check coverage first. Landing tables are not readable by client roles; these views are owner-privileged, so the columns they select are the whole boundary.