Allocation Visualization Helpers¶
The allocation layer’s visualization helpers live in
keeks.allocation.plots: matplotlib-only figures for the objects the
API already returns — bankroll histories, allocation results, weight
matrices, covariance descriptors, the hand-rolled HRP linkage, and scenario
matrices — so examples and benchmarks can render the layer without gluing
matplotlib calls by hand.
Every helper draws one figure and returns the
matplotlib.axes.Axes it drew on, so callers can style further,
save, or embed the result. The helpers are deterministic by construction:
series and legends sort by name, colors come from one small fixed palette
(keeks.allocation.plots.PALETTE), and nothing reads or writes
matplotlib’s global state — figures are bare
matplotlib.figure.Figure objects, never pyplot-managed, which
makes the helpers safe to call inside simulation loops and headless (Agg)
runs.
Bankroll growth and drawdown¶
- keeks.allocation.plots.bankroll_paths(histories: Mapping[str, np.typing.ArrayLike | BankRoll] | np.typing.ArrayLike | BankRoll, log_scale: bool = True) Axes[source]¶
Plot the growth curves of one or more bankroll histories.
Each history becomes one line against its period index. Series sort by name - draw order and legend alike - so a figure is a pure function of its inputs, and colors cycle through
PALETTE. The y-axis is logarithmic by default: compounding bankrolls span orders of magnitude, and zero entries (a bankrupt period) are simply clipped off a log axis. Passlog_scale=Falsefor a linear axis.- Parameters:
histories (mapping or sequence) – Either a mapping of series name to history - each history a sequence of nonnegative bankroll values, or the
BankRollwhosehistoryattribute holds one - or a single bare history, which plots under the name"bankroll".log_scale (bool, default=True) – Whether the y-axis is logarithmic.
- Returns:
The axes the paths were drawn on, one line per series, legend sorted by name.
- Return type:
- Raises:
ValueError – If a history is not a non-empty one-dimensional sequence of finite nonnegative numbers, or the mapping is empty.
Examples
>>> from keeks import BankRoll >>> from keeks.allocation.plots import bankroll_paths >>> bankroll = BankRoll(initial_funds=1000.0) >>> bankroll.deposit(250.0) >>> axes = bankroll_paths({"fixed": bankroll}) >>> [text.get_text() for text in axes.get_legend().get_texts()] ['fixed'] >>> axes.get_yscale() 'log'
- keeks.allocation.plots.drawdown_history(history: np.typing.ArrayLike | BankRoll) Axes[source]¶
Plot the peak-to-trough drawdown curve of a bankroll history.
The drawdown at each entry is the fractional loss from the best bankroll seen so far - zero at every new peak, one at a total wipeout - read as
(running_peak - value) / running_peak. Entries before the first positive value (an unfunded bankroll) carry a zero drawdown: with nothing ever banked there is nothing to lose.- Parameters:
history (sequence of float or object) – The bankroll history - a sequence of nonnegative values, or the
BankRollwhosehistoryattribute holds one.- Returns:
The axes the curve was drawn on, with the drawdown line and a light fill beneath it.
- Return type:
- Raises:
ValueError – If the history is not a non-empty one-dimensional sequence of finite nonnegative numbers.
Examples
>>> from keeks.allocation.plots import drawdown_history >>> axes = drawdown_history([1000.0, 1250.0, 1000.0]) >>> len(axes.lines) 1 >>> round(float(axes.lines[0].get_ydata().max()), 4) 0.2
Weights and risk¶
- keeks.allocation.plots.weight_evolution(weights: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes]) Axes[source]¶
Plot long-only weights through time as a stacked area.
One band per option, stacked bottom-up in option order: the stack’s top edge is the total invested fraction, and the gap down to one is the cash held at zero return. The form fits the layer’s weight contract exactly - weights are nonnegative and sum to no more than one - which is why the y-axis is pinned to
[0, 1].- Parameters:
weights (array-like) – The
(periods, options)matrix of long-only weights, every entry in[0, 1]and every row summing to no more than one withinPROBABILITY_SUM_TOLERANCE.- Returns:
The axes the areas were drawn on, one labeled band per option with colors cycling through
PALETTE.- Return type:
- Raises:
ValueError – If the matrix is not a non-empty two-dimensional sequence of finite numbers, any entry falls outside
[0, 1], or any row sums above1 + PROBABILITY_SUM_TOLERANCE.
Examples
>>> from keeks.allocation.plots import weight_evolution >>> axes = weight_evolution([[0.5, 0.25], [0.25, 0.5]]) >>> [text.get_text() for text in axes.get_legend().get_texts()] ['Option 0', 'Option 1'] >>> axes.get_ylim() == (0.0, 1.0) True
- keeks.allocation.plots.risk_contributions(weights: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes], covariance: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes]) Axes[source]¶
Plot each option’s share of the portfolio’s risk as a bar chart.
The risk contribution of option
iisw_i (Sigma w)_i / (w' Sigma w)- the share of the portfolio variance the option’s own exposure drives. The shares sum to one, and the equal risk contribution portfolio (RiskBudgetingwith equal budgets) is the book where every bar reads1 / N.- Parameters:
weights (array-like) – The long-only weight vector - an
AllocationResult’sweightsor any equivalent sequence, validated like every allocation weight.covariance (array-like) – The covariance matrix of the options’ simple returns, matching the weight vector’s length.
- Returns:
The axes the bars were drawn on, one bar per option in option order.
- Return type:
- Raises:
ValueError – If the weights or the covariance are invalid, they disagree on the option count, or the portfolio carries no variance to attribute.
Examples
Equal weights on diagonal-variance options of 4% and 1% put 80% of the risk in the riskier option:
>>> from keeks.allocation.plots import risk_contributions >>> axes = risk_contributions([0.5, 0.5], [[0.04, 0.0], [0.0, 0.01]]) >>> len(axes.patches) 2 >>> round(float(sum(patch.get_height() for patch in axes.patches)), 12) 1.0
- keeks.allocation.plots.efficient_frontier(mean: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes], covariance: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes], grid: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes]) Axes[source]¶
Plot the long-only mean-variance efficient frontier with the options.
The frontier curve solves
MeanVarianceat every risk aversion ingridand reads each optimum’s(volatility, expected return)pair; the points sort by volatility, so the curve is drawn low-volatility first regardless of the grid’s order. The individual options plot as a scatter at their own(volatility, expected return). Like every scipy-gated method, solving requires thekeeks[allocation]optional extra - the firstMeanVarianceconstruction raises the pointedImportErrorwithout it.- Parameters:
mean (array-like) – The expected simple return of each option.
covariance (array-like) – The covariance matrix of the options’ simple returns, matching the mean’s length.
grid (sequence of float) – The positive risk aversions to sweep, one frontier point each.
- Returns:
The axes the frontier was drawn on: the frontier line plus one labeled point per option.
- Return type:
- Raises:
ImportError – When scipy is not installed; install the
keeks[allocation]extra.ValueError – If the mean, the covariance, or the grid is invalid.
Examples
>>> from keeks.allocation.plots import efficient_frontier >>> axes = efficient_frontier( ... [0.02, 0.01], [[0.04, 0.0], [0.0, 0.01]], [0.5, 1.0, 2.0] ... ) >>> [text.get_text() for text in axes.get_legend().get_texts()] ['Frontier', 'Option 0', 'Option 1']
Structure and tails¶
- keeks.allocation.plots.correlation_heatmap(covariance: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes]) Axes[source]¶
Plot a covariance matrix as a correlation heatmap.
The covariance rescales to a correlation matrix through the diagonal volatilities (clipped to
[-1, 1]against floating-point noise) and draws as a square image on the fixed"RdBu_r"diverging colormap, symmetric about zero on the[-1, 1]color scale.- Parameters:
covariance (array-like) – The covariance matrix of the options’ simple returns. Must have strictly positive variances - the correlation divides by each option’s volatility.
- Returns:
The axes the heatmap was drawn on, with the colorbar attached.
- Return type:
- Raises:
ValueError – If the covariance is invalid or carries a zero variance.
Examples
>>> from keeks.allocation.plots import correlation_heatmap >>> axes = correlation_heatmap([[0.04, 0.004], [0.004, 0.01]]) >>> len(axes.images) 1 >>> axes.images[0].get_clim() (-1.0, 1.0)
- keeks.allocation.plots.dendrogram(linkage: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes]) Axes[source]¶
Plot the cluster tree an agglomerative linkage matrix describes.
The input is the
(n - 1, 4)linkage matrixkeeks.allocation.hierarchical._agglomerative_linkage()produces andHierarchicalRiskParityexposes as itslinkageattribute - one row per merge, leaves at height zero. Each merge draws its u-shaped connector between the two children at the merge distance; leaves sit at consecutive integer positions in the tree’s left-to-right (quasi-diagonal) order. A single option has no merges - the empty(0, 4)matrix - and draws as one tick at height zero.- Parameters:
linkage (array-like) – The
(n - 1, 4)linkage matrix, one row per merge:[first_id, second_id, distance, size].- Returns:
The axes the tree was drawn on, with one tick per option labeled by its option index.
- Return type:
- Raises:
ValueError – If the matrix is not two-dimensional with four columns, is not finite, or carries a negative merge distance.
Examples
>>> from keeks.allocation import HierarchicalRiskParity >>> from keeks.allocation.plots import dendrogram >>> strategy = HierarchicalRiskParity([[0.04, 0.004], [0.004, 0.01]]) >>> axes = dendrogram(strategy.linkage) >>> len(axes.lines) 1 >>> [text.get_text() for text in axes.get_xticklabels()] ['0', '1']
- keeks.allocation.plots.scenario_losses(scenarios: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes], weights: _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float | complex | str | bytes | _NestedSequence[bool | int | float | complex | str | bytes], var: float | None = None, cvar: float | None = None, bins: int = 30) Axes[source]¶
Plot a histogram of portfolio losses over scenarios with tail markers.
The portfolio’s simple returns read off the scenario matrix as
scenarios @ weights; losses are their negation. When given,varandcvardraw vertical markers - the natural callers are the tail statistics of the same distribution, e.g.keeks.allocation.scenarios.MeanCVaR.cvarfor the expected tail loss.- Parameters:
scenarios (array-like) – The
(observations, options)matrix of joint simple returns.weights (array-like) – The long-only weight vector, one entry per scenario column.
var (float, optional) – Where to draw the VaR marker, when given.
cvar (float, optional) – Where to draw the CVaR marker, when given.
bins (int, default=30) – The histogram’s bin count.
- Returns:
The axes the histogram was drawn on, with a legend when at least one marker was drawn.
- Return type:
- Raises:
ValueError – If the scenarios or weights are invalid, they disagree on the option count, the bins are not a positive integer, or a given marker is not finite.
Examples
>>> from keeks.allocation.plots import scenario_losses >>> scenarios = [[0.03, 0.01], [-0.01, 0.02], [0.01, -0.01], [-0.02, -0.02]] >>> axes = scenario_losses(scenarios, [0.5, 0.5], var=0.005, cvar=0.015) >>> [text.get_text() for text in axes.get_legend().get_texts()] ['VaR', 'CVaR']