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. Pass log_scale=False for 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 BankRoll whose history attribute 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:

matplotlib.axes.Axes

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 BankRoll whose history attribute holds one.

Returns:

The axes the curve was drawn on, with the drawdown line and a light fill beneath it.

Return type:

matplotlib.axes.Axes

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 within PROBABILITY_SUM_TOLERANCE.

Returns:

The axes the areas were drawn on, one labeled band per option with colors cycling through PALETTE.

Return type:

matplotlib.axes.Axes

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 above 1 + 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 i is w_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 (RiskBudgeting with equal budgets) is the book where every bar reads 1 / N.

Parameters:
  • weights (array-like) – The long-only weight vector - an AllocationResult’s weights or 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:

matplotlib.axes.Axes

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 MeanVariance at every risk aversion in grid and 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 the keeks[allocation] optional extra - the first MeanVariance construction raises the pointed ImportError without 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:

matplotlib.axes.Axes

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:

matplotlib.axes.Axes

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 matrix keeks.allocation.hierarchical._agglomerative_linkage() produces and HierarchicalRiskParity exposes as its linkage attribute - 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:

matplotlib.axes.Axes

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, var and cvar draw vertical markers - the natural callers are the tail statistics of the same distribution, e.g. keeks.allocation.scenarios.MeanCVaR.cvar for 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:

matplotlib.axes.Axes

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']