Skip to content

Example 12: Portfolio with risk and transaction fees ​

1. Overview ​

This bounded context allocates an integer fund among five investment products, including a risk-free deposit, maximizing net yield subject to a fee-inclusive budget and normalized risk limit. The page records the current function-symbol mechanisms and the Kotlin/Rust Big-M difference.

The current product parameters are:

ProductYield (ri)Risk (qi)Fee rate (pi)Minimum fee (mi)
P10.280.040.08103
P20.210.0150.02198
P30.230.050.04552
P40.250.0260.0440
P5 (deposit)0.05000

1. Dependent Contexts ​

  1. None. The portfolio model is self-contained.

2. Concepts / Entities ​

1. Investment Product ​

Each product accepts an integer investment and has a yield rate, risk rate, proportional fee rate, and minimum fee.

ri: yield rate of product i.

qi: risk rate/coefficient of product i.

pi: proportional transaction-fee rate of product i.

mi: minimum transaction fee charged when product i is used.

Product P5 is the source's risk-free deposit: q5=p5=m5=0 and r5=0.05.

2. Fund ​

F: total available fund, F=1,000,000 yuan.

RMax: normalized risk limit, RMax=0.02.


3. Variables ​

1. Decision Variables ​

xi: integer yuan invested in product i, dimension yuan, domain Z≥0, ∀i∈P. Kotlin uses UIntVariable1; Rust uses VariableCombination1D<UInteger>.

2. Auxiliary Variables ​

ai: binary assignment indicator generated by BinaryzationFunction, dimensionless, intended to be 1 exactly when xi>0.

Premiumi: transaction-fee function output generated by MaxFunction; it is constrained below by proportional and minimum-fee expressions.


4. Predicates ​

1. Investment Status ​

Invested(i): true when xi>0.

RiskFree(i): true for the fifth source product, whose risk and fee parameters are zero.

2. Fee Branch ​

RateFeeDominates(i): true when pixi≥miai.

MinimumFeeDominates(i): true when miai≥pixi.


5. Sets ​

1. Product Categories ​

P: universal set of five investment products.

P+: invested-product subset, P+={i∈P∣Invested(i)}.

PRF: risk-free product subset; in the sample PRF={5}.

2. Entity Pairs / Relations ​

No pair relation is needed; all fee, risk, and yield expressions are indexed by a single product.


6. Intermediate Values ​

1. Investment Assignment Indicator ​

Description: The binary function marks whether product i receives a positive investment. It is implemented by the source's BinaryzationFunction and its generated mechanism constraints.

ai={1,xi>0,0,xi≤0,∀i∈P.

2. Transaction Fee ​

Description: The fee is the larger of the proportional fee and the minimum fee activated by a positive investment. The current MaxFunction generates lower-bound mechanism constraints; budget and yield make the value tight at an optimum.

Premiumi=max(pixi,miai),∀i∈P.

3. Normalized Risk ​

Description: Total risk exposure divided by the total fund, using the decimal risk rates supplied by the source.

Risk=∑i∈PqixiF.

4. Net Yield ​

Description: Gross product yield minus the transaction fees.

Yield=∑i∈P(rixi−Premiumi).

5. Budget Usage ​

Description: Principal plus fees consumed by the portfolio. Kotlin writes this sum directly in its budget constraint; Rust registers a funds symbol combination for the same expression.

BudgetUsage=∑i∈P(xi+Premiumi).

7. Assertions ​

1. Assignment Is Binary ​

Description: Every product's assignment output is either zero or one and follows the positive-investment predicate.

∀i∈P(ai∈{0,1}∧(ai=1⟺xi>0)).

2. Fee Lower Bounds ​

Description: Each fee is at least both the proportional fee and the active minimum fee; at an optimum it equals their maximum.

∀i∈P(Premiumi≥pixi∧Premiumi≥miai∧Premiumi=max(pixi,miai)).

The equality is the intended optimum semantics; the generated MaxFunction mechanism supplies the lower bounds.

3. Fee-Inclusive Budget ​

Description: The entire fund, including transaction fees, is allocated exactly.

BudgetUsage=F.

4. Current Result Scope ​

Description: The current structure test does not pin a numeric allocation. Any older result that used only S2/S4 belongs to a former four-product presentation and must not be presented as the optimum of this five-product model.


8. Constraints ​

1. Investment Indicator (投资标记约束) ​

Description: The binaryization mechanism links each indicator to whether its integer investment is positive. The Kotlin helper uses its default Big-M behavior; Rust passes funds as the explicit Big-M and guards the zero-funds case in its risk coefficient.

s.t.ai∈{0,1},ai=1⟺xi>0,∀i∈P.

2. Maximum Transaction Fee (交易费用最大值约束) ​

Description: The MaxFunction mechanism enforces both candidate fee lower bounds. Because fees consume budget and reduce the objective, the optimum uses the smallest feasible value.

s.t.Premiumi≥pixi,Premiumi≥miai,∀i∈P.

3. Fee-Inclusive Budget (含费用预算约束) ​

Description: Investment principal plus all transaction fees must equal the available fund.

s.t.∑i∈P(xi+Premiumi)=F.

4. Risk Limit (风险上限约束) ​

Description: Normalized portfolio risk cannot exceed the configured limit.

s.t.∑i∈PqixiF≤RMax.

5. Investment Domain (投资金额定义域约束) ​

Description: Each investment amount is a non-negative integer number of yuan.

s.t.xi∈Z≥0,∀i∈P.

9. Objective Function (if applicable) ​

Description: Maximize net yield after transaction fees.

maxYield.

10. Algorithm References ​

The function mechanisms are referenced inline; no separate algorithm document is needed for this example.

Algorithm NameFile PathReferenced InBrief Description
BinaryzationFunctionospf-kotlin-example/src/main/.../Demo12.kt and ospf-rust-example/src/core/demo12.rsSections 6.1, 8.1Generates a binary positive-activity indicator.
MaxFunctionSame source filesSections 6.2, 8.2Generates lower bounds for the maximum of two linear fee expressions.

11. Ubiquitous Language ​

TermSymbolDefinition
InvestmentxiInteger principal assigned to product i.
AssignmentaiBinary positive-investment indicator.
Premium/feePremiumiTransaction fee charged for product i.
RiskRiskFund-normalized risk exposure.
Net yieldYieldGross yield less fees.
Risk-free depositPRFSource product P5 with zero risk and zero fee.

12. Design Decisions ​

DecisionAlternativesRationaleDate
Keep P5 in the product setUse only the four risky productsP5 is an explicit risk-free product in both current sources2026-09-08
Include fees in the budgetUse ∑i(1+pi)xi=FThat former formula omits minimum fees and does not represent the current model2026-09-08
Document mechanism semantics separately from API detailsClaim identical Big-M constructionKotlin relies on default Big-M; Rust calls with_big_m(..., funds) and guards zero funds2026-09-08

Minimal current model-building snippets ​

The product data, funds, and maxRisk come from Demo12.kt/demo12.rs. Both tabs include the investment variables, assignment and fee functions, risk/yield expressions, objective, budget, and risk constraints. The mechanisms' generated inequalities are source-owned.

kotlin
// `products`, `funds`, `maxRisk`, and `flt64Converter` come from Demo12.kt.
val metaModel = LinearMetaModel<Flt64>("demo12", converter = flt64Converter)
val x = UIntVariable1("x", Shape1(products.size))
val assignment = LinearIntermediateSymbols1<Flt64>("assignment", Shape1(products.size)) { i, _ ->
    LinearFunctionSymbolAdapter(
        delegate = BinaryzationFunction(
            polynomial = LinearPolynomial(x[i]),
            converter = flt64Converter,
            name = "assignment_$i"
        ),
        converter = flt64Converter
    )
}
val premium = LinearIntermediateSymbols1<Flt64>("premium", Shape1(products.size)) { i, _ ->
    val product = products[i]
    LinearFunctionSymbolAdapter(
        delegate = MaxFunction(
            listOf(
                LinearPolynomial(product.premium * x[i]),
                LinearPolynomial(product.minPremium * assignment[i])
            ),
            converter = flt64Converter,
            name = "premium_$i"
        ),
        converter = flt64Converter
    )
}
val risk = LinearExpressionSymbol(
    sum(products.map { p -> p.risk * x[p] / funds }), name = "risk"
)
val yield = LinearExpressionSymbol(
    sum(products.map { p -> p.yield * x[p] - premium[p] }), name = "yield"
)
metaModel.add(x); metaModel.add(assignment); metaModel.add(premium)
metaModel.add(risk); metaModel.add(yield)
metaModel.maximize(yield, "yield")
metaModel.addConstraint(sum(products.map { p -> x[p] + premium[p] }) eq funds)
metaModel.addConstraint(risk leq maxRisk)
rust
// `products`, `funds`, and `max_risk` come from demo12.rs.
let n = products.len();
let mut model = MetaModel::<f64>::new("demo12");
let x: VariableCombination1D<UInteger> =
    VariableCombination1D::new(Shape::new([n]), "x");
let x_idx = model.register_combination(&x)?;
let assignment_fn = SymbolCombination::new(Shape::new([n]), "assignment", |i, _| {
    BinaryzationFunction::with_big_m(
        i as u64 + 100, &products[i].name,
        ospf_rust_core::symbol::flatten::Linear::new(
            vec![ospf_rust_core::symbol::flatten::LinearMonomial::new(1.0, x_idx[i])],
            0.0,
        ),
        funds,
    )
});
model.add_symbol_combination(&assignment_fn)?;
let premium_fn = SymbolCombination::new(Shape::new([n]), "premium", |i, _| {
    let assign_idx = assignment_fn.symbol_polynomial(i).monomials()[0].var_index();
    MaxFunction::new(
        i as u64 + 200, &products[i].name,
        vec![
            ospf_rust_core::symbol::flatten::Linear::new(
                vec![ospf_rust_core::symbol::flatten::LinearMonomial::new(
                    products[i].premium_rate, x_idx[i],
                )], 0.0,
            ),
            ospf_rust_core::symbol::flatten::Linear::new(
                vec![ospf_rust_core::symbol::flatten::LinearMonomial::new(
                    products[i].min_premium, assign_idx,
                )], 0.0,
            ),
        ], false,
    )
});
model.add_symbol_combination(&premium_fn)?;
let premium_idx: Vec<_> = (0..n).map(|i|
    premium_fn.symbol_polynomial(i).monomials()[0].var_index()
).collect();
let yield_expr = flat_map1_indexed("yield", products, |i, product| {
    ospf_rust_core::symbol::flatten::Linear::new(vec![
        ospf_rust_core::symbol::flatten::LinearMonomial::new(product.yield_rate, x_idx[i]),
        ospf_rust_core::symbol::flatten::LinearMonomial::new(-1.0, premium_idx[i]),
    ], 0.0)
}, |_, product| product.name.clone());
let funds_expr = flat_map1_indexed("funds", products, |i, product| {
    ospf_rust_core::symbol::flatten::Linear::new(vec![
        ospf_rust_core::symbol::flatten::LinearMonomial::new(1.0, x_idx[i]),
        ospf_rust_core::symbol::flatten::LinearMonomial::new(1.0, premium_idx[i]),
    ], 0.0)
}, |_, product| product.name.clone());
let risk_expr = flat_map1_indexed("risk", products, |i, product| {
    let coefficient = if funds.abs() < 1e-10 { 0.0 } else { product.risk_rate / funds };
    ospf_rust_core::symbol::flatten::Linear::new(vec![
        ospf_rust_core::symbol::flatten::LinearMonomial::new(coefficient, x_idx[i]),
    ], 0.0)
}, |_, product| product.name.clone());
model.add_symbol_combination(&yield_expr)?;
model.add_symbol_combination(&funds_expr)?;
model.add_symbol_combination(&risk_expr)?;
let mut yield_terms = Vec::new();
for i in 0..n {
    for monomial in yield_expr.symbol_polynomial(i).monomials() {
        yield_terms.push((monomial.var_index(), *monomial.coefficient()));
    }
}
let mut funds_terms = Vec::new();
for i in 0..n {
    for monomial in funds_expr.symbol_polynomial(i).monomials() {
        funds_terms.push((monomial.var_index(), *monomial.coefficient()));
    }
}
let mut risk_terms = Vec::new();
for i in 0..n {
    for monomial in risk_expr.symbol_polynomial(i).monomials() {
        risk_terms.push((monomial.var_index(), *monomial.coefficient()));
    }
}
model.add_linear_objective(&yield_terms, "yield");
model.set_objective_category(ObjectiveCategory::Maximum);
model.add_linear_constraint(&funds_terms, ConstraintRelation::Equal, funds, "funds")?;
model.add_linear_constraint(
    &risk_terms, ConstraintRelation::LessEqual, max_risk, "risk",
)?;

13. Change Log ​

VersionChangeReason
2026-09-08Reorganized the page into the domain-model template; documented all fee/risk intermediates, mechanism constraints, Big-M differences, and Kotlin/Rust tabsMatch the current five-product Demo12 implementations without publishing an unverified allocation

Source and verification ​