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:
| Product | Yield ( | Risk ( | Fee rate ( | Minimum fee ( |
|---|---|---|---|---|
| P1 | 0.28 | 0.04 | 0.08 | 103 |
| P2 | 0.21 | 0.015 | 0.02 | 198 |
| P3 | 0.23 | 0.05 | 0.045 | 52 |
| P4 | 0.25 | 0.026 | 0.04 | 40 |
| P5 (deposit) | 0.05 | 0 | 0 | 0 |
1. Dependent Contexts
- 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.
Product P5 is the source's risk-free deposit:
2. Fund
3. Variables
1. Decision Variables
UIntVariable1; Rust uses VariableCombination1D<UInteger>.
2. Auxiliary Variables
BinaryzationFunction, dimensionless, intended to be 1 exactly when
MaxFunction; it is constrained below by proportional and minimum-fee expressions.
4. Predicates
1. Investment Status
Invested(i): true when
RiskFree(i): true for the fifth source product, whose risk and fee parameters are zero.
2. Fee Branch
RateFeeDominates(i): true when
MinimumFeeDominates(i): true when
5. Sets
1. Product Categories
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 BinaryzationFunction and its generated mechanism constraints.
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.
3. Normalized Risk
Description: Total risk exposure divided by the total fund, using the decimal risk rates supplied by the source.
4. Net Yield
Description: Gross product yield minus the transaction fees.
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.
7. Assertions
1. Assignment Is Binary
Description: Every product's assignment output is either zero or one and follows the positive-investment predicate.
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.
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.
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.
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.
3. Fee-Inclusive Budget (含费用预算约束)
Description: Investment principal plus all transaction fees must equal the available fund.
4. Risk Limit (风险上限约束)
Description: Normalized portfolio risk cannot exceed the configured limit.
5. Investment Domain (投资金额定义域约束)
Description: Each investment amount is a non-negative integer number of yuan.
9. Objective Function (if applicable)
Description: Maximize net yield after transaction fees.
10. Algorithm References
The function mechanisms are referenced inline; no separate algorithm document is needed for this example.
| Algorithm Name | File Path | Referenced In | Brief Description |
|---|---|---|---|
BinaryzationFunction | ospf-kotlin-example/src/main/.../Demo12.kt and ospf-rust-example/src/core/demo12.rs | Sections 6.1, 8.1 | Generates a binary positive-activity indicator. |
MaxFunction | Same source files | Sections 6.2, 8.2 | Generates lower bounds for the maximum of two linear fee expressions. |
11. Ubiquitous Language
| Term | Symbol | Definition |
|---|---|---|
| Investment | Integer principal assigned to product | |
| Assignment | Binary positive-investment indicator. | |
| Premium/fee | Transaction fee charged for product | |
| Risk | Fund-normalized risk exposure. | |
| Net yield | Gross yield less fees. | |
| Risk-free deposit | Source product P5 with zero risk and zero fee. |
12. Design Decisions
| Decision | Alternatives | Rationale | Date |
|---|---|---|---|
| Keep P5 in the product set | Use only the four risky products | P5 is an explicit risk-free product in both current sources | 2026-09-08 |
| Include fees in the budget | Use | That former formula omits minimum fees and does not represent the current model | 2026-09-08 |
| Document mechanism semantics separately from API details | Claim identical Big-M construction | Kotlin relies on default Big-M; Rust calls with_big_m(..., funds) and guards zero funds | 2026-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.
// `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)// `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
| Version | Change | Reason |
|---|---|---|
| 2026-09-08 | Reorganized the page into the domain-model template; documented all fee/risk intermediates, mechanism constraints, Big-M differences, and Kotlin/Rust tabs | Match the current five-product Demo12 implementations without publishing an unverified allocation |