trader.reporting.concentration module¶
Sector concentration and return-correlation measurement (issue #65).
docs/ideas.md item 7 named this “the most under-measured risk here”: theme-driven discovery structurally concentrates a book into a handful of correlated macro bets wearing different tickers, and neither max_position_pct nor max_total_exposure_pct notices that the names move together. This module is a measurement only — it computes numbers for an existing report to print. It does not gate an order, is never called from execution/ or a strategy, and imports nothing from brokers/ or execution/ itself.
Two independent measurements, matching the issue’s two asks:
sector_exposure — held positions’ market value, grouped by sector, as a fraction of the book. A symbol whose sector lookup fails or is unclassified (an ETF, mainly) is grouped under UNKNOWN_SECTOR, an explicit and visible bucket added into the total the same way a real sector is — never silently dropped, and never folded into a real sector as if it were known to belong there.
compute_correlations — pairwise Pearson correlation of daily returns across held positions, computed from the existing bar cache (no new data source). A symbol with too little history to build a return series (a fresh listing, a data outage) is left out of every pair rather than treated as uncorrelated with everything else — “not computed” is not the same claim as “zero risk”.
A third helper, sector_distribution_by_count, answers the “discovered” half of the issue’s first ask: a discovered symbol carries no market value until it becomes a position, so weighting it by dollars is meaningless — counting it once each is the only honest way to show whether the candidate pool itself is concentrated before any of it is bought.
- trader.reporting.concentration.DEFAULT_CORRELATION_LOOKBACK_DAYS = 90¶
Default lookback for the correlation window. 90 calendar days is roughly 60 trading sessions — enough for a Pearson correlation to mean something without reaching so far back that a recently discovered, thinly-traded name has no history at all inside the window.
- trader.reporting.concentration.UNKNOWN_SECTOR = 'Unknown'¶
The explicit bucket for a symbol whose sector could not be determined — no coverage (an ETF), a lookup failure, or an unclassified new listing. Never blank, and never folded into a real sector.
- class trader.reporting.concentration.PortfolioCorrelation(pairwise, average_pairwise)[source]¶
Bases:
objectEvery computable pair, and their average.
average_pairwise is None — never 0.0 — when fewer than two symbols had enough return history to compute even one pair: “no correlation data” and “measured, and it’s zero” are different claims, and this module never lets the first read as the second.
- Parameters:
pairwise (tuple[SymbolCorrelation, ...])
average_pairwise (float | None)
- pairwise: tuple[SymbolCorrelation, ...]¶
- average_pairwise: float | None¶
- class trader.reporting.concentration.SectorCount(sector, count, pct_of_total, symbols)[source]¶
Bases:
objectOne sector’s (or UNKNOWN_SECTOR’s) share of a symbol list, by count.
- Parameters:
sector (str)
count (int)
pct_of_total (Decimal)
symbols (tuple[str, ...])
- sector: str¶
- count: int¶
- pct_of_total: Decimal¶
- symbols: tuple[str, ...]¶
- class trader.reporting.concentration.SectorExposure(sector, market_value, pct_of_total, symbols)[source]¶
Bases:
objectOne sector’s (or UNKNOWN_SECTOR’s) share of held market value.
- Parameters:
sector (str)
market_value (Decimal)
pct_of_total (Decimal)
symbols (tuple[str, ...])
- sector: str¶
- market_value: Decimal¶
- pct_of_total: Decimal¶
- symbols: tuple[str, ...]¶
- class trader.reporting.concentration.SymbolCorrelation(symbol_a, symbol_b, correlation)[source]¶
Bases:
objectPearson correlation of daily returns between two held symbols.
- Parameters:
symbol_a (str)
symbol_b (str)
correlation (float)
- symbol_a: str¶
- symbol_b: str¶
- correlation: float¶
- trader.reporting.concentration.compute_correlations(positions, bar_cache, *, now, lookback_days=90)[source]¶
Pairwise correlation across held positions’ daily returns.
Bars come from the existing BarCache — no new data source, per the issue’s own constraint. A symbol whose bar fetch fails, or that has too few bars in the window (_MIN_RETURNS_FOR_CORRELATION), is left out of every pair rather than defaulting to any particular correlation value.
- Parameters:
- Return type:
- trader.reporting.concentration.sector_distribution_by_count(symbols, sector_provider)[source]¶
A symbol list’s sector breakdown, weighted one-per-symbol.
For a set of symbols that carry no market value yet — discovered candidates that are not (or not all) held positions — counting is the only weighting that makes sense; see the module docstring. Duplicate symbols in symbols are deduplicated first, so a candidate discovered on more than one theme is not counted twice.
- Parameters:
symbols (Sequence[str])
sector_provider (SectorProvider)
- Return type:
list[SectorCount]
- trader.reporting.concentration.sector_exposure(positions, sector_provider)[source]¶
Held positions’ market value, grouped by sector, richest first.
Weighted by market_value — this answers “how much of the book sits in one sector”, not “how many names”. A sector lookup failure for one symbol costs only that symbol (same isolation discipline as discovery and the analyst-consensus fetch): it is grouped under UNKNOWN_SECTOR and logged, never allowed to abort the rest of the book’s grouping.
- Parameters:
positions (Sequence[Position])
sector_provider (SectorProvider)
- Return type:
list[SectorExposure]