Skip to content

Portfolio Re-balancing

Flag holdings that exceed concentration limits, group stocks that tend to move together, and compare rebalanced portfolios by expected return and risk under normal and stressed markets.

Goals
OptimizePrioritize
Reasoning types
PrescriptiveRules-basedGraph
Experience level
Intermediate
Browse files

What this template is for

Investment managers need to keep portfolios within concentration and compliance limits while weighing expected return against risk. This template flags concentration problems, groups correlated stocks, calculates constrained allocations, and compares them under normal and stressed markets. Adapt the investment universe, limits, objectives, and stress assumptions for your workflow.

Explore the model

Explore the model

The model connects investors and their accounts to holdings, stocks, sectors, and transactions. Rules inspect the current portfolio, graph reasoning groups correlated stocks, and prescriptive reasoning chooses a re-balanced portfolio.

UserAccountHoldingTransactionStockSectorRegimeScenarioFrontierPointowned byheld inposition inbelongs tomade bypairs have covariance inhas quantity inhas regimeis for scenario
UserAccountHoldingTransactionStockSectorRegimeScenarioFrontierPointowned byheld inposition inbelongs tomade bypairs have covariance inhas quantity inhas regimeis for scenario
User

An investor who owns the accounts holding stock positions. The model uses the investor’s risk score and flagged transactions to identify high-risk traders.

Properties

NameType
user_id (identity)Integer
user_nameString
risk_scoreFloat

Relationships

owned by
Account is owned by User
made by
Transaction was made by User
high-risk trader
User is a high-risk trader
Preview source datausers.csv · 6 rows; showing 4

Investor names and risk scores.

View data/users.csv

idnamerisk_score
1Alice Chen0.85
2Bob Martinez0.45
3Carol Davis0.62
4Dan Wilson0.38
Account

A brokerage or retirement account owned by an investor.

Properties

NameType
account_id (identity)Integer
acct_user_idInteger
account_typeString
balanceFloat

Relationships

owned by
Account is owned by User
held in
Holding is held in Account
Preview source dataaccounts.csv · 4 rows

Account ownership, type, and balance.

View data/accounts.csv

iduser_idaccount_typebalance
11brokerage100000.00
22retirement200000.00
33brokerage150000.00
44retirement80000.00
Holding

A stock position held in an account.

Properties

NameType
holding_id (identity)Integer
holding_account_idInteger
holding_stock_idInteger
holding_quantityFloat
purchase_priceFloat
holding_valueFloat

Relationships

held in
Holding is held in Account
position in
Holding is a position in Stock
overconcentrated
Holding is overconcentrated
in concentrated sector
Holding is in a concentrated sector position
Preview source dataholdings.csv · 15 rows; showing 4

Current stock positions by account.

View data/holdings.csv

idaccount_idstock_idquantitypurchase_price
111120150.00
21280200.00
3143090.00
4164085.00
Transaction

An investor transaction with an amount, category, and flagged status from the input data. The model counts flagged transactions by investor as part of its high-risk trader check.

Properties

NameType
transaction_id (identity)Integer
txn_user_idInteger
txn_amountFloat
txn_categoryString
is_flagged_valFloat

Relationships

made by
Transaction was made by User
Preview source datatransactions.csv · 21 rows; showing 4

Investor transactions and existing flagged indicators.

View data/transactions.csv

iduser_idamountcategoryis_flagged
115200.00wire_transfertrue
218100.00wire_transfertrue
313400.00tradingtrue
4112000.00withdrawaltrue
Stock

A stock available for investment, with an expected return and covariance values. Covariance describes how its returns tend to move with those of other stocks, helping the model group related stocks and measure portfolio risk.

Properties

NameType
index (identity)Integer
stock_tickerString
stock_sectorString
returnsFloat
covarFloat
stock_varianceFloat
stock_volatilityFloat
stock_correlationFloat
cluster_idInteger
stock_sharpeFloat
cluster_max_sharpeFloat
regime_covarFloat
quantityFloat

Relationships

position in
Holding is a position in Stock
belongs to
Stock belongs to Sector
cluster representative
Stock is a cluster representative
not a cluster representative
Stock is not a cluster representative
Preview source datareturns.csv · 8 rows; showing 4

Stock identifiers, sectors, and expected returns.

View data/returns.csv

indextickersectorreturns
1AAPLTechnology0.08
2MSFTTechnology0.07
3GOOGLTechnology0.09
4JNJHealthcare0.05
Sector

A market sector used to measure and constrain concentration.

Properties

NameType
sector_name (identity)String

Relationships

belongs to
Stock belongs to Sector
Regime

A market condition used to evaluate portfolio risk with base or crisis covariance.

Properties

NameType
regime_name (identity)String
Scenario

A budget and market-regime combination solved as one portfolio optimization scenario.

Properties

NameType
name (identity)String
budgetFloat
regimeRegime
FrontierPoint

A point on a scenario's efficient frontier, including its return, risk, exact marginal risk, knee indicator, and base-to-crisis volatility change.

Properties

NameType
scenario_label (identity)String
eps_label (identity)String
scenarioScenario
fp_kInteger
fp_returnFloat
fp_riskFloat
fp_marginalFloat
fp_is_kneeBoolean
fp_vol_baseFloat
fp_vol_crisisFloat
fp_vol_gapFloat
fp_vol_gap_pctFloat
Account is owned by User

Connects each account to the investor who owns it.

Fields

FieldValue
Nameowned by
FromAccount
ToUser
Holding is held in Account

Connects a position to the account that contains it.

Fields

FieldValue
Nameheld in
FromHolding
ToAccount
Holding is a position in Stock

Connects a position to the security it represents.

Fields

FieldValue
Nameposition in
FromHolding
ToStock
Stock belongs to Sector

Groups stocks for sector exposure checks and constraints.

Fields

FieldValue
Namebelongs to
FromStock
ToSector
Transaction was made by User

Connects activity to the investor evaluated by compliance rules.

Fields

FieldValue
Namemade by
FromTransaction
ToUser
Holding is overconcentrated

Flags a holding whose value exceeds the configured share of its account balance.

Fields

FieldValue
Nameoverconcentrated
Applies toHolding
Holding is in a concentrated sector position

Flags a holding whose sector exposure exceeds the configured share of its account balance.

Fields

FieldValue
Namein concentrated sector
Applies toHolding
User is a high-risk trader

Flags an investor whose risk score and flagged transaction count exceed the configured thresholds.

Fields

FieldValue
Namehigh-risk trader
Applies toUser
Stock is a cluster representative

Marks the highest-Sharpe stock selected to represent its correlation cluster.

Fields

FieldValue
Namecluster representative
Applies toStock
Stock is not a cluster representative

Marks a stock excluded from optimization because another stock represents its correlation cluster.

Fields

FieldValue
Namenot a cluster representative
Applies toStock
Stock pairs have covariance in Regime

A Float-valued property that connects Stock to Regime.

Fields

FieldValue
Nameregime_covar
SignatureStock, Stock, Regime -> Float
FromStock
ToRegime
Stock has quantity in Scenario

A Float-valued property that connects Stock to Scenario.

Fields

FieldValue
Namequantity
SignatureStock, Scenario -> Float
FromStock
ToScenario
Scenario has regime

A concept-valued property that connects Scenario to Regime.

Fields

FieldValue
Nameregime
FromScenario
ToRegime
FrontierPoint is for scenario

A concept-valued property that connects FrontierPoint to Scenario.

Fields

FieldValue
Namescenario
FromFrontierPoint
ToScenario
users.csv

Investor names and risk scores.

Populates

Model item
User

Source

Path
data/users.csv
Kind
Local CSV

Preview: 6 rows; showing 4

idnamerisk_score
1Alice Chen0.85
2Bob Martinez0.45
3Carol Davis0.62
4Dan Wilson0.38
accounts.csv

Account ownership, type, and balance.

Populates

Model item
Account

Source

Path
data/accounts.csv
Kind
Local CSV

Preview: 4 rows

iduser_idaccount_typebalance
11brokerage100000.00
22retirement200000.00
33brokerage150000.00
44retirement80000.00
holdings.csv

Current stock positions by account.

Populates

Model item
Holding

Source

Path
data/holdings.csv
Kind
Local CSV

Preview: 15 rows; showing 4

idaccount_idstock_idquantitypurchase_price
111120150.00
21280200.00
3143090.00
4164085.00
transactions.csv

Investor transactions and existing flagged indicators.

Populates

Model item
Transaction

Source

Path
data/transactions.csv
Kind
Local CSV

Preview: 21 rows; showing 4

iduser_idamountcategoryis_flagged
115200.00wire_transfertrue
218100.00wire_transfertrue
313400.00tradingtrue
4112000.00withdrawaltrue
returns.csv

Stock identifiers, sectors, and expected returns.

Populates

Model item
Stock

Source

Path
data/returns.csv
Kind
Local CSV

Preview: 8 rows; showing 4

indextickersectorreturns
1AAPLTechnology0.08
2MSFTTechnology0.07
3GOOGLTechnology0.09
4JNJHealthcare0.05
covar.csv

Pairwise stock covariance values.

Populates

Model item
Stock

Source

Path
data/covar.csv
Kind
Local CSV

Preview: 64 rows; showing 4

ijcovar
110.018641
120.012500
130.014200
140.002100

Download and run the template

Before you start, install Python 3.10 or later and get access to a Snowflake account with the RAI Native App. Graph and Prescriptive reasoning are in Public Preview; ask your RelationalAI support representative to enable Prescriptive reasoning. Preview features are for evaluation and testing, not production applications. The template pins relationalai==1.9.0 in pyproject.toml.

Use this sequence to run the bundled example:

  1. Download the template

    Download the ZIP, unzip it, and enter the template directory:

    Terminal window
    unzip portfolio_balancing.zip
    cd portfolio_balancing
  2. Create a Python environment

    Create and activate a virtual environment, then update pip:

    Terminal window
    python -m venv .venv
    source .venv/bin/activate
    python -m pip install --upgrade pip
  3. Install the template

    Install the dependencies pinned in pyproject.toml:

    Terminal window
    python -m pip install .
  4. Configure your project

    Use the configuration builder in Start building with PyRel to create raiconfig.yaml in the template directory and verify your connection.

  5. Run the template

    Run the bundled script from the template directory:

    Terminal window
    python portfolio_balancing.py
    Output
    STAGE 1: COMPLIANCE ANALYSIS (rules)
    STAGE 2: GRAPH -- Covariance Clustering (Louvain)
    Louvain communities: 5 cluster(s)
    STAGE 3: BI-OBJECTIVE OPTIMIZATION
    Status: OPTIMAL
    SENSITIVITY-GUIDED FRONTIER (reference 'base_1000', 6-solve budget per method)
    dichotomic 6 202.2972 <- tightest
    STAGE 4: CRISIS REGIME STRESS TEST
    KNEE PORTFOLIO ALLOCATIONS BY SCENARIO
    base_1000 (budget=1000, regime=base, point=p3)
    expected return=80.46, volatility=83.33
    Each knee is a candidate portfolio, not a recommendation. The amounts apply to
    the sample scenario, not to a specific account.

    The run flags four holdings and two sectors, groups eight stocks into five correlation clusters, and selects p3 as the candidate frontier point in each of six budget-and-market scenarios. Estimated crisis volatility is 22% to 30% above the base regime.

    These candidates are starting points for review, not recommendations or trades for a specific account. See runbook.md for the full workflow and result interpretation.

See how it works

These seven steps follow the model from its schema and data through the four stages. The final step shows how to read the results and choose a candidate portfolio. The current holdings and calculated portfolios share Stock, but Scenario is not linked to User, Account, or Holding. The output shows sample portfolios, not trades for a specific account.

Hover over dotted-underlined code terms to highlight their source. You can also focus or tap each term.

Define the shared model

The schema defines nine concepts for the current portfolio, market data, scenarios, and results. This excerpt shows the declarations for the primary concepts used to optimize the portfolio.

Regime represents a market condition, such as base or crisis. Scenario combines a budget with a regime. Stock.regime_covar records the covariance between each pair of stocks for a regime. Stock.x_quantity represents how much of a stock the optimizer assigns to a scenario. FrontierPoint represents one risk and return result. FrontierPoint.scenario links that result to its scenario.

model/schema.py (lines 71-86)
Regime = model.Concept("Regime", identify_by={"regime_name": String})
Scenario = model.Concept("Scenario", identify_by={"name": String})
Scenario.budget = model.Property(f"{Scenario} has {Float:budget}")
Scenario.regime = model.Property(f"{Scenario} in {Regime}")
Stock.regime_covar = model.Property(
f"{Stock} and {Stock} in {Regime} have {Float:regime_covar}"
)
Stock.x_quantity = model.Property(f"{Stock} in {Scenario} has {Float:quantity}")
FrontierPoint = model.Concept(
"FrontierPoint",
identify_by={"scenario_label": String, "eps_label": String},
)
FrontierPoint.scenario = model.Property(f"{FrontierPoint} for {Scenario}")
model/schema.py (lines 71-86)
Regime = model.Concept("Regime", identify_by={"regime_name": String})
Scenario = model.Concept("Scenario", identify_by={"name": String})
Scenario.budget = model.Property(f"{Scenario} has {Float:budget}")
Scenario.regime = model.Property(f"{Scenario} in {Regime}")
Stock.regime_covar = model.Property(
f"{Stock} and {Stock} in {Regime} have {Float:regime_covar}"
)
Stock.x_quantity = model.Property(f"{Stock} in {Scenario} has {Float:quantity}")
FrontierPoint = model.Concept(
"FrontierPoint",
identify_by={"scenario_label": String, "eps_label": String},
)
FrontierPoint.scenario = model.Property(f"{FrontierPoint} for {Scenario}")

Load and connect the data

First, the loader creates stocks from the returns file. It also adds covariance values and links each stock to a sector. Next, it loads the current portfolio and connects its records to those stocks.

model.data() reads the holding rows into the model. Holding.new() creates a holding from each row. Holding.account links each holding to its account. Holding.stock matches each holding to a stock by ID. This match connects each current position to the return and covariance data used to calculate new portfolio allocations.

model/source.py (lines 52-63)
holding_data = model.data(read_csv(DATA_DIR / "holdings.csv"))
model.define(
holding := Holding.new(holding_id=holding_data["id"]),
holding.account_id(holding_data["account_id"]),
holding.stock_id(holding_data["stock_id"]),
holding.quantity(holding_data["quantity"]),
holding.purchase_price(holding_data["purchase_price"]),
)
model.define(Holding.account(Account)).where(
Holding.account_id == Account.account_id
)
model.define(Holding.stock(Stock)).where(Holding.stock_id == Stock.index)
model/source.py (lines 52-63)
holding_data = model.data(read_csv(DATA_DIR / "holdings.csv"))
model.define(
holding := Holding.new(holding_id=holding_data["id"]),
holding.account_id(holding_data["account_id"]),
holding.stock_id(holding_data["stock_id"]),
holding.quantity(holding_data["quantity"]),
holding.purchase_price(holding_data["purchase_price"]),
)
model.define(Holding.account(Account)).where(
Holding.account_id == Account.account_id
)
model.define(Holding.stock(Stock)).where(Holding.stock_id == Stock.index)

Stage 1: Check the current portfolio

Stage 1 checks the current portfolio for three problems. This example calculates each holding's value, finds its account, and flags the holding when its value is more than 15% of the account balance.

Stage 1 also flags an account when one sector makes up more than 30% of its balance. It flags a user when the user's risk score is above 0.8 and the user has more than five flagged transactions. In the sample data, these checks flag four holdings, two account and sector combinations, and two users.

These flags give investment managers a short list of holdings, sector exposures, and users that may need review.

portfolio_balancing.py (lines 84-92)
# Derived holding value = quantity * purchase_price.
model.define(Holding.value(Holding.quantity * Holding.purchase_price))
# Rule 1: Overconcentrated holdings -- position value > POSITION_LIMIT of balance.
AccountR1 = Account.ref()
model.where(
Holding.account(AccountR1),
Holding.value > POSITION_LIMIT * AccountR1.balance,
).define(Holding.is_overconcentrated())
portfolio_balancing.py (lines 84-92)
# Derived holding value = quantity * purchase_price.
model.define(Holding.value(Holding.quantity * Holding.purchase_price))
# Rule 1: Overconcentrated holdings -- position value > POSITION_LIMIT of balance.
AccountR1 = Account.ref()
model.where(
Holding.account(AccountR1),
Holding.value > POSITION_LIMIT * AccountR1.balance,
).define(Holding.is_overconcentrated())

Stage 2: Group related stocks

Stage 2 groups stocks that tend to move together. It creates a corr_graph.Edge when the absolute value of their correlation is at least 0.3. corr_graph.louvain() then places connected stocks into clusters. Stock.cluster records the cluster assigned to each stock.

Within each cluster, the model calculates each stock's Sharpe ratio, which compares return with volatility. It marks the stock with the highest ratio as the representative and excludes the other stocks from the new allocations. In the sample data, four connections form five clusters, and five of the eight stocks become representatives.

Using one representative from each cluster reduces the chance that a new portfolio will include several stocks that behave alike.

portfolio_balancing.py (lines 271-286)
stock_i_ref = Stock.ref()
stock_j_ref = Stock.ref()
corr_ref = Float.ref()
model.define(corr_graph.Edge.new(src=stock_i_ref, dst=stock_j_ref)).where(
stock_i_ref.correlation(stock_j_ref, corr_ref),
stock_i_ref.index < stock_j_ref.index,
math_abs(corr_ref) >= CORR_THRESHOLD,
)
# Louvain community detection -- stored as Stock.cluster (integer id).
community = corr_graph.louvain()
cluster_label = Integer.ref("cluster_label")
stock_clust_ref = Stock.ref()
model.define(stock_clust_ref.cluster(cluster_label)).where(
community(stock_clust_ref, cluster_label)
)
portfolio_balancing.py (lines 271-286)
stock_i_ref = Stock.ref()
stock_j_ref = Stock.ref()
corr_ref = Float.ref()
model.define(corr_graph.Edge.new(src=stock_i_ref, dst=stock_j_ref)).where(
stock_i_ref.correlation(stock_j_ref, corr_ref),
stock_i_ref.index < stock_j_ref.index,
math_abs(corr_ref) >= CORR_THRESHOLD,
)
# Louvain community detection -- stored as Stock.cluster (integer id).
community = corr_graph.louvain()
cluster_label = Integer.ref("cluster_label")
stock_clust_ref = Stock.ref()
model.define(stock_clust_ref.cluster(cluster_label)).where(
community(stock_clust_ref, cluster_label)
)

Stage 3: Calculate portfolio allocations

Stage 3 creates six scenarios from three budgets and two market regimes. For each scenario, it calculates how much of each stock to include. Each portfolio must use its full budget. No amount can be negative, no stock or sector can exceed 30% of the budget, and stocks not selected in Stage 2 get an amount of zero.

In the code, Stock.regime_covar supplies the covariance values for the scenario's regime. Stock.x_quantity supplies the amount assigned to each stock. The model uses these values to find the portfolio with the lowest variance, the measure of risk in this example, for a required return. For each result, it also calculates how much more variance is needed when the required return increases by one unit.

Stage 3 starts with the portfolios that have the lowest risk and the highest return. It compares three ways to choose four points between them. For Stage 4, it uses the dichotomic method. This method compares how quickly risk is increasing at two existing points to estimate where the risk and return curve bends most. It calculates a portfolio at that return level and repeats until it has six points. Together, these points show the lowest risk available across a range of returns.

portfolio_balancing.py (lines 690-697)
problem.minimize(
sum(regime_cov_val * x_qty * x_qty_paired)
.where(
Stock.regime_covar(PairedStock, Scenario.regime, regime_cov_val),
Stock.x_quantity(Scenario, x_qty),
PairedStock.x_quantity(Scenario, x_qty_paired),
)
)
portfolio_balancing.py (lines 690-697)
problem.minimize(
sum(regime_cov_val * x_qty * x_qty_paired)
.where(
Stock.regime_covar(PairedStock, Scenario.regime, regime_cov_val),
Stock.x_quantity(Scenario, x_qty),
PairedStock.x_quantity(Scenario, x_qty_paired),
)
)

Stage 4: Summarize the results

Stage 4 starts with stage3_solves, which contains the cached allocation table and shadow prices for each point calculated in Stage 3. For every scenario and point, evaluate_return() and evaluate_risk() calculate the summary measures. marginal_risk_per_return records how much more variance is needed when the required return increases by one unit.

After the code marks the knee and adds the stress comparison, FrontierPoint.new() stores each completed summary in the model. A frontier point represents one calculated portfolio using measures of risk and return. The individual stock amounts remain in the Stage 3 results. is_knee marks the point before variance begins to increase much faster. vol_base and vol_crisis record volatility for the matching base and crisis results.

These summaries let the final queries compare the calculated portfolios and identify the portfolio associated with the FrontierPoint marked as is_knee. The next step shows its stock amounts.

portfolio_balancing.py (lines 1088-1158)
fp_rows = []
for sn in scenario_names:
slopes = []
rows_for_sn = []
for k, (label, allocation_df, shadow) in enumerate(stage3_solves):
marginal = float(shadow.get(sn, 0.0)) # EXACT dual; 0 at the min-risk anchor
slopes.append(marginal)
rows_for_sn.append(
{
"scenario_label": sn,
"eps_label": label,
"k": k,
"return": evaluate_return(allocation_df, sn),
"risk": evaluate_risk(allocation_df, sn),
"marginal_risk_per_return": marginal,
"is_knee": False,
}
)
37 collapsed lines
# Knee = the last point before the frontier slope accelerates most: the largest RATIO
# jump between consecutive exact duals (lambda_j / lambda_{j-1}), marking point j-1 so
# "cost jumps beyond this point". This applies the ratio-knee principle from
# rai-prescriptive-results ("Knee from the exact dual sequence") to the exact
# duals rather than finite-difference secants -- the knee is NOT where the absolute slope is
# highest (that is always the last point). The min-risk anchor (k=0) declares no return
# floor so its dual is structurally 0; the scan starts at j=2 to skip that 0->lambda_1
# transition, whose "jump" would be an absolute magnitude rather than a rate-of-change.
knee_idx, max_jump = None, 0.0
for j in range(2, len(slopes)):
prev, curr = slopes[j - 1], slopes[j]
if prev <= 1e-9:
continue
jump = curr / prev
if jump > max_jump:
max_jump, knee_idx = jump, j - 1
if knee_idx is not None:
rows_for_sn[knee_idx]["is_knee"] = True
fp_rows.extend(rows_for_sn)
# Pair base and crisis rows by (budget, eps_label) so vol_base / vol_crisis carry on
# BOTH the base-regime row and its matching crisis-regime row.
risk_by_key = {(r["scenario_label"], r["eps_label"]): r["risk"] for r in fp_rows}
for r in fp_rows:
budget_suffix = r["scenario_label"].split("_", 1)[1]
base_risk = risk_by_key.get((f"base_{budget_suffix}", r["eps_label"]))
crisis_risk = risk_by_key.get((f"crisis_{budget_suffix}", r["eps_label"]))
vol_base = base_risk ** 0.5 if base_risk is not None else 0.0
vol_crisis = crisis_risk ** 0.5 if crisis_risk is not None else 0.0
vol_gap = vol_crisis - vol_base
r["vol_base"] = vol_base
r["vol_crisis"] = vol_crisis
r["vol_gap"] = vol_gap
r["vol_gap_pct"] = (vol_gap / vol_base * 100.0) if vol_base > 1e-9 else 0.0
# FrontierPoint facts -- one row per (scenario, point). The marginal is the exact
# dual everywhere (0 at the min-risk anchor), so a single-pass load works (no NaN).
fp_data = model.data(DataFrame(fp_rows))
model.define(
fp := FrontierPoint.new(
scenario_label=fp_data["scenario_label"],
eps_label=fp_data["eps_label"],
),
fp.k(fp_data["k"]),
fp.return_value(fp_data["return"]),
fp.risk(fp_data["risk"]),
fp.marginal_risk_per_return(fp_data["marginal_risk_per_return"]),
fp.is_knee(fp_data["is_knee"]),
fp.vol_base(fp_data["vol_base"]),
fp.vol_crisis(fp_data["vol_crisis"]),
fp.vol_gap(fp_data["vol_gap"]),
fp.vol_gap_pct(fp_data["vol_gap_pct"]),
)
portfolio_balancing.py (lines 1088-1105)
fp_rows = []
for sn in scenario_names:
slopes = []
rows_for_sn = []
for k, (label, allocation_df, shadow) in enumerate(stage3_solves):
marginal = float(shadow.get(sn, 0.0)) # EXACT dual; 0 at the min-risk anchor
slopes.append(marginal)
rows_for_sn.append(
{
"scenario_label": sn,
"eps_label": label,
"k": k,
"return": evaluate_return(allocation_df, sn),
"risk": evaluate_risk(allocation_df, sn),
"marginal_risk_per_return": marginal,
"is_knee": False,
}
)
portfolio_balancing.py (lines 1143-1158)
fp_data = model.data(DataFrame(fp_rows))
model.define(
fp := FrontierPoint.new(
scenario_label=fp_data["scenario_label"],
eps_label=fp_data["eps_label"],
),
fp.k(fp_data["k"]),
fp.return_value(fp_data["return"]),
fp.risk(fp_data["risk"]),
fp.marginal_risk_per_return(fp_data["marginal_risk_per_return"]),
fp.is_knee(fp_data["is_knee"]),
fp.vol_base(fp_data["vol_base"]),
fp.vol_crisis(fp_data["vol_crisis"]),
fp.vol_gap(fp_data["vol_gap"]),
fp.vol_gap_pct(fp_data["vol_gap_pct"]),
)

Choose a candidate portfolio

After Stage 4, frontier_df lists the calculated portfolios for every budget and market regime. Each row shows expected return, variance, the extra variance needed for another unit of required return, and whether its FrontierPoint is marked as is_knee. A second table compares volatility under base and crisis conditions.

To choose a candidate portfolio, first select the budget and market regime you want to plan for. Then compare return and risk across that scenario's rows. The portfolio associated with the FrontierPoint marked as is_knee is a useful place to start because it is the last point before variance begins to rise much faster for each additional unit of required return. It is a candidate to review, not a recommendation.

The final loop uses _extract_frontier_point_allocations() to retrieve that portfolio's stock amounts from Stage 3 for each of the six scenarios. Amount shows how much of the scenario's budget is assigned to a stock. Weight shows the stock's share of the budget. The output excerpt shows the base portfolio for a budget of 1,000. To turn it into a rebalancing plan, link the scenario to an account and compare these target amounts with its current holdings.

portfolio_balancing.py (lines 1268-1307)
knee_rows = frontier_df[
(frontier_df["scenario_label"] == sn)
& frontier_df["is_knee"]
]
if len(knee_rows) != 1:
print(
f"\n {sn}: No unique knee point was found. "
"Choose a point from the frontier table above."
)
continue
knee_row = knee_rows.iloc[0]
label, allocations = _extract_frontier_point_allocations(
stage3_solves,
knee_row,
sn,
)
13 collapsed lines
meta = scenario_meta[sn]
budget = meta["budget"]
volatility = float(knee_row["risk"]) ** 0.5
print(
f"\n {sn} (budget={budget:.0f}, regime={meta['regime']}, point={label})"
)
print(
f" expected return={float(knee_row['return']):.2f}, "
f"volatility={volatility:.2f}"
)
print(f" {'Ticker':<8}{'Amount':>12}{'Weight':>10}")
print(f" {'-' * 30}")
ordered_allocations = sorted(
allocations.items(),
key=lambda item: (-item[1], stock_ticker_map.get(item[0], str(item[0]))),
)
for stock_index, quantity in ordered_allocations:
ticker = stock_ticker_map.get(stock_index, f"Stock {stock_index}")
print(
f" {ticker:<8}{float(quantity):>12.2f}"
f"{float(quantity) / budget:>9.1%}"
)
portfolio_balancing.py (lines 1268-1284)
knee_rows = frontier_df[
(frontier_df["scenario_label"] == sn)
& frontier_df["is_knee"]
]
if len(knee_rows) != 1:
print(
f"\n {sn}: No unique knee point was found. "
"Choose a point from the frontier table above."
)
continue
knee_row = knee_rows.iloc[0]
label, allocations = _extract_frontier_point_allocations(
stage3_solves,
knee_row,
sn,
)
portfolio_balancing.py (lines 1298-1307)
ordered_allocations = sorted(
allocations.items(),
key=lambda item: (-item[1], stock_ticker_map.get(item[0], str(item[0]))),
)
for stock_index, quantity in ordered_allocations:
ticker = stock_ticker_map.get(stock_index, f"Stock {stock_index}")
print(
f" {ticker:<8}{float(quantity):>12.2f}"
f"{float(quantity) / budget:>9.1%}"
)
Output
base_1000 (budget=1000, regime=base, point=p3)
expected return=80.46, volatility=83.33
Ticker Amount Weight
------------------------------
GOOGL 300.00 30.0%
XOM 278.57 27.9%
PFE 234.07 23.4%
JPM 135.49 13.5%
PG 51.87 5.2%