docs/features/allocations/sota-target-model-spec.md
Status: Draft Date: 2026-05-07 Audience: Product, frontend, backend, desktop, web, and addon engineers
This document specifies a greenfield allocation target and rebalancing advisor for Wealthfolio. It should not be constrained by any existing pull request, prototype schema, or prototype UI. Existing Wealthfolio allocation and taxonomy services are treated only as platform capabilities.
The goal is to turn Wealthfolio from a current-allocation viewer into an allocation advisor that can answer:
This is product and engineering design, not investment advice. Wealthfolio should help users model and execute their own rules without implying that any asset mix is suitable for every user.
Wealthfolio already has the core foundation needed for this feature:
The new feature should build on that foundation while keeping target modeling, rebalancing policy, funding policy, and trade planning as separate domain concepts.
Expected architecture flow:
Frontend
-> adapters/tauri or adapters/web
-> Tauri command or Axum REST route
-> crates/core allocation-advisor services
-> crates/storage-sqlite repositories
-> SQLite
Targets are the source of truth. Current allocation is observed state. Targets are desired state.
Rebalancing is policy-driven. "When to act" must be separate from "how to trade".
Cash should be first-class. Cash can be a target sleeve, available funding, a reserve, and a trade constraint. These are related but not identical.
Taxable accounts need restraint. Selling to rebalance may create tax impact. Wealthfolio should prefer contribution-based correction when it satisfies the policy.
Drafts before action. The system proposes plans and trades. Users review, exclude, export, or execute through supported workflows.
Explain every recommendation. Every trade recommendation needs a plain reason and the constraints applied.
Multi-dimensional analysis is not automatically multi-dimensional targeting. Asset class can be a primary target while geography, sector, risk, and account placement act as guardrails unless the optimizer explicitly supports simultaneous constraints.
The model must support assets not currently owned. A user must be able to target a sleeve, category, or model allocation with zero current holdings.
The feature should follow common portfolio-management practices used by major brokerages, robo-advisors, and advisor tools.
An investor defines an intended asset mix. Market movement, contributions, withdrawals, income, and price changes cause the current mix to drift from that target. Rebalancing restores or moves the portfolio closer to the intended risk target.
Common target types:
Industry sources commonly describe three trigger families:
Vanguard describes calendar, threshold, and calendar-plus-threshold methods, and highlights the tradeoff between tracking error and transaction costs. Fidelity describes the same three approaches and notes that rebalancing can involve selling overweight positions and buying underweight ones, while new contributions may help avoid taxable sells.
Before selling, the system should try to use:
This is especially important in taxable accounts because selling winners can realize gains.
State-of-the-art rebalancing tools account for:
Schwab's advisor rebalancing tooling describes rule-based rebalancing, tax-aware household-level rebalancing, tax-loss harvesting, cash management, and order approval workflows as professional-grade capabilities.
Value averaging is a funding strategy. It defines a target portfolio value path over time and calculates the contribution needed to stay on that path. It is not an allocation target and not a rebalance trigger.
In Wealthfolio it belongs under Funding Policy:
The output of value averaging is an available contribution amount. The allocation/rebalancing engine then decides where that contribution should go.
The SOTA model has eight independent but connected entities.
AllocationTarget
-> AllocationTargetWeight[]
-> TargetGuardrail[]
-> HoldingTarget[]
-> RebalancePolicy
-> FundingPolicy
-> ExecutionPolicy
-> RebalanceRun[]
-> TradeDraft[]
Defines the desired portfolio model for a scope.
Fields:
| Field | Type | Notes |
|---|---|---|
| id | string | UUID |
| name | string | User-visible name |
| scope_type | enum | portfolio, account, account_group |
| scope_id | string nullable | Null for whole portfolio |
| base_currency | string | ISO currency code |
| objective | enum nullable | growth, balanced, income, preservation, custom |
| model_type | enum | template, custom, imported |
| version | integer | Increment on material changes |
| effective_from | date nullable | For scheduled/glide changes |
| rebalance_goal | enum | nearest_band, exact_target |
| min_trade_amount | decimal text | Future planner minimum trade amount |
| whole_shares_only | boolean | Future planner execution constraint |
| created_at | datetime | UTC |
| updated_at | datetime | UTC |
| archived_at | datetime nullable | Null for normal targets; set when archived |
Rules:
Defines a target sleeve in a primary taxonomy.
Fields:
| Field | Type | Notes |
|---|---|---|
| id | string | UUID |
| target_id | string | Parent AllocationTarget |
| taxonomy_id | string | Usually asset_classes for v1 |
| category_id | string | References taxonomy category |
| parent_node_id | string nullable | Enables nested sleeve targets |
| target_bps | integer | 0 to 10000 |
| min_bps | integer nullable | Lower tolerance bound |
| max_bps | integer nullable | Upper tolerance bound |
| drift_threshold_bps | integer nullable | Override policy default |
| rebalance_priority | integer | Lower number acts first |
| is_required | boolean | Required in target even if no holdings |
| is_locked | boolean | UI cannot auto-adjust this row |
| created_at | datetime | UTC |
| updated_at | datetime | UTC |
Rules:
Defines secondary constraints that should be monitored or optionally enforced.
Examples:
Fields:
| Field | Type | Notes |
|---|---|---|
| id | string | UUID |
| target_id | string | Parent AllocationTarget |
| guardrail_type | enum | taxonomy, holding, account, cash, concentration |
| taxonomy_id | string nullable | Required for taxonomy guardrails |
| category_id | string nullable | Category for taxonomy guardrails |
| asset_id | string nullable | Asset for holding guardrails |
| account_id | string nullable | Account-specific constraint |
| min_bps | integer nullable | Percentage minimum |
| max_bps | integer nullable | Percentage maximum |
| min_amount | decimal nullable | Currency minimum |
| max_amount | decimal nullable | Currency maximum |
| severity | enum | info, warning, block |
| enforcement | enum | monitor_only, constrain_plan, block_plan |
Rules:
Defines optional instrument targets inside an allocation sleeve.
Fields:
| Field | Type | Notes |
|---|---|---|
| id | string | UUID |
| allocation_node_id | string | Parent AllocationTargetWeight |
| asset_id | string | Asset/instrument |
| target_bps | integer nullable | Target inside the sleeve |
| min_bps | integer nullable | Lower band inside sleeve |
| max_bps | integer nullable | Upper band inside sleeve |
| buy_priority | integer nullable | Lower number buys first |
| sell_priority | integer nullable | Lower number sells first |
| substitute_group_id | string nullable | For equivalent ETFs/funds |
| is_locked | boolean | Prevent auto-adjust |
| is_buyable | boolean | Can receive buys |
| is_sellable | boolean | Can be sold by plans |
Rules:
Defines when Wealthfolio should prompt the user to rebalance.
Fields:
| Field | Type | Notes |
|---|---|---|
| id | string | UUID |
| target_id | string | Parent AllocationTarget |
| trigger_type | enum | manual, calendar, threshold, hybrid |
| review_frequency | enum nullable | weekly, monthly, quarterly, semiannual, annual |
| next_review_date | date nullable | Used for calendar/hybrid |
| default_band_bps | integer | Example: 500 for +/-5% |
| band_type | enum | absolute, relative |
| rebalance_to | enum | exact_target, nearest_band |
| notify_on_breach | boolean | UI/notification trigger |
| require_confirmation | boolean | Always true for v1 |
Rules:
Defines how new money enters the plan.
Fields:
| Field | Type | Notes |
|---|---|---|
| id | string | UUID |
| target_id | string | Parent AllocationTarget |
| funding_mode | enum | none, manual_cash, recurring_contribution, dca, value_averaging |
| cash_source | enum | user_input, account_cash, dividends_interest, external |
| default_cash_amount | decimal nullable | Optional prefill |
| reserve_amount | decimal nullable | Cash to keep uninvested |
| contribution_frequency | enum nullable | weekly, monthly, quarterly, annual |
| start_date | date nullable | For scheduled funding |
| end_condition | enum nullable | none, date, target_value |
| target_value_path | json nullable | Value averaging path parameters |
| max_top_up_amount | decimal nullable | Cap contribution |
| overflow_action | enum nullable | hold_cash, next_period, sell_excess |
| fractional_units | boolean | Whether fractional shares are allowed |
Rules:
Defines how a rebalance plan is allowed to create trades.
Fields:
| Field | Type | Notes |
|---|---|---|
| id | string | UUID |
| target_id | string | Parent AllocationTarget |
| scenario_mode | enum | cash_flow_only, sell_to_rebalance, hybrid |
| allow_sells | boolean | False for cash-flow-only |
| tax_mode | enum | ignore, aware, strict |
| lot_selection | enum nullable | fifo, lifo, hifo, loss_first, long_term_first |
| wash_sale_check | boolean | Requires lot/history support |
| min_trade_amount | decimal | Skip tiny trades |
| min_trade_bps | integer nullable | Skip tiny portfolio changes |
| max_turnover_bps | integer nullable | Cap total trade volume |
| max_realized_gain | decimal nullable | User-defined cap |
| whole_shares_only | boolean | False if fractional allowed |
| asset_location | enum | ignore, prefer_tax_efficient, enforce_rules |
| blocked_asset_ids | json | Do-not-trade list |
| preferred_asset_ids | json | Buy candidates by sleeve |
Rules:
Immutable snapshot of one calculation.
Fields:
| Field | Type | Notes |
|---|---|---|
| id | string | UUID |
| target_id | string | Allocation target used |
| target_version | integer | Version at calculation time |
| scope_type | enum | Copied from target |
| scope_id | string nullable | Copied from target |
| run_status | enum | draft, accepted, exported, canceled, stale |
| scenario_mode | enum | Scenario calculated |
| base_currency | string | Currency |
| portfolio_value | decimal | Snapshot value |
| available_cash | decimal | Deployable input |
| max_drift_bps_before | integer | Before plan |
| max_drift_bps_after | integer | Estimated after plan |
| turnover_bps | integer | Estimated turnover |
| estimated_tax_impact | decimal nullable | If available |
| explanation | json | Constraints and summary |
| created_at | datetime | UTC |
Rules:
User-reviewable proposed trade.
Fields:
| Field | Type | Notes |
|---|---|---|
| id | string | UUID |
| run_id | string | Parent RebalanceRun |
| action | enum | buy, sell |
| account_id | string | Account where trade occurs |
| asset_id | string | Asset/instrument |
| symbol | string | Display symbol snapshot |
| quantity | decimal | Shares/units |
| estimated_price | decimal | Quote used |
| estimated_amount | decimal | Quantity * price |
| sleeve_category_id | string | Target sleeve reason |
| reason | string | Human-readable reason |
| tax_lot_ids | json nullable | If lot-level support exists |
| estimated_gain | decimal nullable | Tax estimate |
| wash_sale_warning | boolean | Warning flag |
| is_excluded | boolean | User excluded from plan |
| exclusion_reason | string nullable | Optional |
Rules:
For each target scope:
current_bps = current_value / total_scope_value * 10000
target_bps = target allocation weight
drift_bps = current_bps - target_bps
value_delta = current_value - target_value
target_value = total_scope_value * target_bps / 10000
Interpretation:
For absolute bands:
min_bps = target_bps - band_bps
max_bps = target_bps + band_bps
For relative bands:
min_bps = target_bps * (1 - relative_band)
max_bps = target_bps * (1 + relative_band)
Rules:
Manual:
Calendar:
Threshold:
Hybrid:
Default:
Exact target:
Nearest band:
Default:
The plan screen should support scenario comparison.
Cash-flow only:
Sell to rebalance:
Hybrid:
For each underweight sleeve:
Resolve buy candidates:
Remove ineligible candidates:
Allocate budget:
Convert amount to quantity:
Skip trades below minimum trade amount or minimum trade bps.
For each overweight sleeve:
Resolve sell candidates:
Sort by execution policy:
Respect constraints:
Generate sell drafts and recompute buy budget.
Value averaging should be a FundingPolicy mode.
Inputs:
Output:
target_portfolio_value_for_period
required_top_up = target_portfolio_value_for_period - current_portfolio_value
available_cash = clamp(required_top_up, 0, max_top_up_amount)
overflow = max(current_portfolio_value - target_portfolio_value_for_period, 0)
The resulting available_cash is passed to the selected execution scenario.
The feature should have three main surfaces.
Allocation Advisor
-> Monitor current vs target
-> Drill into sleeves and holdings
-> See drift, bands, guardrails, and trigger state
Targets & Policy
-> Choose scope
-> Pick or build target model
-> Tune target sleeves and bands
-> Configure rebalance trigger
-> Configure funding policy
-> Configure execution policy
-> Save draft or select
Rebalance Plan
-> Compare scenarios
-> Explain before/after drift
-> Show tax/turnover/cash constraints
-> Review, exclude, save, export, or execute draft trades
Recommended advisor navigation:
Avoid:
Purpose:
Primary controls:
Core content:
Row display:
| Column | Meaning |
|---|---|
| Sleeve | Category name and color |
| Current | Current percent and value |
| Target | Target percent |
| Band | Min/max or threshold |
| Drift | Current - target |
| Action | Inspect holdings |
Empty states:
Purpose:
Recommended layout:
Scope
Model
Target allocation
Guardrails
Rebalance trigger
Funding policy
Execution policy
Save & select
Purpose:
Top summary:
Scenario tabs:
Each scenario must show:
Trade table:
| Column | Meaning |
|---|---|
| Include | Checkbox to exclude from draft |
| Action | Buy or sell |
| Account | Account where trade occurs |
| Symbol | Ticker |
| Name | Asset name |
| Sleeve | Target sleeve |
| Quantity | Shares/units |
| Amount | Estimated trade amount |
| Tax | Gain/loss/warning |
| Reason | Why this trade exists |
Footer actions:
Use precise labels:
| Use | Avoid |
|---|---|
| Targets & Policy | Strategy |
| Rebalance trigger | Rebalance strategy |
| Scenario | Mode |
| Cash-flow only | Buy only |
| Sell to rebalance | Buy & sell |
| Hybrid | Combined |
| Funding policy | Contribution strategy |
| Execution policy | Advanced settings |
| Drift | Difference |
| Out of band | Bad allocation |
Recommended core modules:
crates/core/src/portfolio/allocation_advisor/
mod.rs
model.rs
target_service.rs
policy_service.rs
drift_service.rs
trigger_service.rs
funding_service.rs
rebalancing_service.rs
trade_planner.rs
validation.rs
traits.rs
Recommended storage modules:
crates/storage-sqlite/src/portfolio/allocation_advisor/
mod.rs
model.rs
repository.rs
Thin application layers:
apps/tauri/src/commands/allocation_advisor.rs
apps/server/src/api/allocation_advisor.rs
apps/frontend/src/adapters/shared/allocation-advisor.ts
TargetService:
PolicyService:
DriftService:
TriggerService:
FundingService:
RebalancingService:
TradePlanner:
Every new command must exist in both runtime paths:
Do not add frontend calls that only work in desktop mode unless the UI clearly disables them in web mode.
Suggested frontend adapter functions:
export async function listAllocationTargets(
scope?: TargetScope,
): Promise<AllocationTarget[]>;
export async function getAllocationTarget(
id: string,
): Promise<AllocationTargetDetail>;
export async function saveAllocationTarget(
input: SaveAllocationTargetInput,
): Promise<AllocationTargetDetail>;
export async function selectAllocationTarget(
id: string,
): Promise<AllocationTarget>;
export async function archiveAllocationTarget(id: string): Promise<void>;
export async function getAllocationAdvisorState(
input: AdvisorStateInput,
): Promise<AdvisorState>;
export async function evaluateRebalanceTrigger(
targetId: string,
): Promise<TriggerEvaluation>;
export async function calculateRebalanceScenarios(
input: RebalanceScenarioInput,
): Promise<RebalanceScenarioSet>;
export async function saveRebalanceDraft(runId: string): Promise<RebalanceRun>;
export async function exportRebalanceDraft(
runId: string,
format: "csv",
): Promise<ExportResult>;
Use a new migration set. Names below are logical names; final Diesel table names can follow repository conventions.
CREATE TABLE allocation_targets (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
scope_type TEXT NOT NULL,
scope_id TEXT,
base_currency TEXT NOT NULL,
objective TEXT,
model_type TEXT NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
effective_from TEXT,
rebalance_goal TEXT NOT NULL DEFAULT 'nearest_band'
CHECK (rebalance_goal IN ('nearest_band', 'exact_target')),
min_trade_amount TEXT NOT NULL DEFAULT '0',
whole_shares_only INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
archived_at TEXT
);
Indexes:
(scope_type, scope_id, archived_at)CREATE TABLE allocation_target_weights (
id TEXT PRIMARY KEY NOT NULL,
target_id TEXT NOT NULL,
taxonomy_id TEXT NOT NULL,
category_id TEXT NOT NULL,
parent_node_id TEXT,
target_bps INTEGER NOT NULL,
min_bps INTEGER,
max_bps INTEGER,
drift_threshold_bps INTEGER,
rebalance_priority INTEGER NOT NULL DEFAULT 100,
is_required INTEGER NOT NULL DEFAULT 1,
is_locked INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (target_id) REFERENCES allocation_targets(id) ON DELETE CASCADE,
FOREIGN KEY (parent_node_id) REFERENCES allocation_target_weights(id) ON DELETE CASCADE
);
Indexes:
(target_id)(taxonomy_id, category_id)CREATE TABLE target_guardrails (
id TEXT PRIMARY KEY NOT NULL,
target_id TEXT NOT NULL,
guardrail_type TEXT NOT NULL,
taxonomy_id TEXT,
category_id TEXT,
asset_id TEXT,
account_id TEXT,
min_bps INTEGER,
max_bps INTEGER,
min_amount TEXT,
max_amount TEXT,
severity TEXT NOT NULL,
enforcement TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (target_id) REFERENCES allocation_targets(id) ON DELETE CASCADE
);
CREATE TABLE holding_targets (
id TEXT PRIMARY KEY NOT NULL,
allocation_node_id TEXT NOT NULL,
asset_id TEXT NOT NULL,
target_bps INTEGER,
min_bps INTEGER,
max_bps INTEGER,
buy_priority INTEGER,
sell_priority INTEGER,
substitute_group_id TEXT,
is_locked INTEGER NOT NULL DEFAULT 0,
is_buyable INTEGER NOT NULL DEFAULT 1,
is_sellable INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (allocation_node_id) REFERENCES allocation_target_weights(id) ON DELETE CASCADE,
UNIQUE(allocation_node_id, asset_id)
);
CREATE TABLE rebalance_policies (
id TEXT PRIMARY KEY NOT NULL,
target_id TEXT NOT NULL UNIQUE,
trigger_type TEXT NOT NULL,
review_frequency TEXT,
next_review_date TEXT,
default_band_bps INTEGER NOT NULL,
band_type TEXT NOT NULL,
rebalance_to TEXT NOT NULL,
notify_on_breach INTEGER NOT NULL DEFAULT 0,
require_confirmation INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (target_id) REFERENCES allocation_targets(id) ON DELETE CASCADE
);
CREATE TABLE funding_policies (
id TEXT PRIMARY KEY NOT NULL,
target_id TEXT NOT NULL UNIQUE,
funding_mode TEXT NOT NULL,
cash_source TEXT NOT NULL,
default_cash_amount TEXT,
reserve_amount TEXT,
contribution_frequency TEXT,
start_date TEXT,
end_condition TEXT,
target_value_path_json TEXT,
max_top_up_amount TEXT,
overflow_action TEXT,
fractional_units INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (target_id) REFERENCES allocation_targets(id) ON DELETE CASCADE
);
CREATE TABLE execution_policies (
id TEXT PRIMARY KEY NOT NULL,
target_id TEXT NOT NULL UNIQUE,
scenario_mode TEXT NOT NULL,
allow_sells INTEGER NOT NULL,
tax_mode TEXT NOT NULL,
lot_selection TEXT,
wash_sale_check INTEGER NOT NULL DEFAULT 0,
min_trade_amount TEXT NOT NULL,
min_trade_bps INTEGER,
max_turnover_bps INTEGER,
max_realized_gain TEXT,
whole_shares_only INTEGER NOT NULL DEFAULT 0,
asset_location TEXT NOT NULL,
blocked_asset_ids_json TEXT NOT NULL DEFAULT '[]',
preferred_asset_ids_json TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (target_id) REFERENCES allocation_targets(id) ON DELETE CASCADE
);
CREATE TABLE rebalance_runs (
id TEXT PRIMARY KEY NOT NULL,
target_id TEXT NOT NULL,
target_version INTEGER NOT NULL,
scope_type TEXT NOT NULL,
scope_id TEXT,
run_status TEXT NOT NULL,
scenario_mode TEXT NOT NULL,
base_currency TEXT NOT NULL,
portfolio_value TEXT NOT NULL,
available_cash TEXT NOT NULL,
max_drift_bps_before INTEGER NOT NULL,
max_drift_bps_after INTEGER NOT NULL,
turnover_bps INTEGER NOT NULL,
estimated_tax_impact TEXT,
explanation_json TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (target_id) REFERENCES allocation_targets(id)
);
CREATE TABLE trade_drafts (
id TEXT PRIMARY KEY NOT NULL,
run_id TEXT NOT NULL,
action TEXT NOT NULL,
account_id TEXT NOT NULL,
asset_id TEXT NOT NULL,
symbol TEXT NOT NULL,
quantity TEXT NOT NULL,
estimated_price TEXT NOT NULL,
estimated_amount TEXT NOT NULL,
sleeve_category_id TEXT NOT NULL,
reason TEXT NOT NULL,
tax_lot_ids_json TEXT,
estimated_gain TEXT,
wash_sale_warning INTEGER NOT NULL DEFAULT 0,
is_excluded INTEGER NOT NULL DEFAULT 0,
exclusion_reason TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (run_id) REFERENCES rebalance_runs(id) ON DELETE CASCADE
);
Recommended page structure:
apps/frontend/src/pages/allocation-advisor/
allocation-advisor-page.tsx
targets-policy-page.tsx
rebalance-plan-page.tsx
components/
scope-selector.tsx
target-selector.tsx
target-model-picker.tsx
target-allocation-editor.tsx
target-band-row.tsx
guardrail-editor.tsx
rebalance-trigger-editor.tsx
funding-policy-editor.tsx
execution-policy-editor.tsx
advisor-composition-chart.tsx
advisor-drift-table.tsx
rebalance-scenario-tabs.tsx
before-after-allocation.tsx
trade-draft-table.tsx
hooks/
use-allocation-targets.ts
use-advisor-state.ts
use-rebalance-scenarios.ts
use-policy-mutations.ts
UX implementation notes:
@wealthfolio/ui and shadcn patterns.react-hook-form and zod for target/policy forms.Use camelCase DTOs at the frontend boundary and Rust snake_case internally.
interface AdvisorState {
scope: TargetScope;
selectedTarget: AllocationTargetSummary | null;
portfolioValue: Money;
trigger: TriggerEvaluation | null;
rows: AllocationAdvisorRow[];
guardrails: GuardrailEvaluation[];
missingClassifications: MissingClassificationSummary[];
}
interface AllocationAdvisorRow {
taxonomyId: string;
categoryId: string;
categoryName: string;
color: string;
currentBps: number;
targetBps: number;
minBps: number;
maxBps: number;
driftBps: number;
currentValue: Money;
targetValue: Money;
valueDelta: Money;
status: "in_band" | "underweight" | "overweight" | "not_targeted";
isRequired: boolean;
isZeroCurrent: boolean;
}
interface RebalanceScenarioInput {
targetId: string;
scenarioModes: Array<"cash_flow_only" | "sell_to_rebalance" | "hybrid">;
availableCash?: string;
baseCurrency: string;
asOfDate?: string;
}
interface RebalanceScenarioSet {
targetId: string;
targetVersion: number;
generatedAt: string;
scenarios: RebalanceScenario[];
}
interface RebalanceScenario {
runId: string;
scenarioMode: "cash_flow_only" | "sell_to_rebalance" | "hybrid";
summary: RebalanceScenarioSummary;
beforeRows: AllocationAdvisorRow[];
afterRows: AllocationAdvisorRow[];
trades: TradeDraft[];
constraintsApplied: ConstraintExplanation[];
warnings: PlanWarning[];
}
Target:
Allocation target weights:
Guardrails:
Funding:
Execution:
Plan:
Ship:
Do not ship:
Ship:
Ship:
Ship when required data exists:
Ship:
Functional:
UX:
Technical:
Rust unit tests:
Repository tests:
Frontend tests:
Integration tests:
Manual QA:
These decisions can be made during implementation without changing the core model: