分离股票池精确预算比例与展示基点并持久化状态

This commit is contained in:
boris
2026-09-19 21:09:27 +08:00
parent 3c70a9273b
commit 5924e43f3d
7 changed files with 171 additions and 10 deletions
+14 -5
View File
@@ -484,6 +484,8 @@ pub struct StockPoolDecisionConstraints {
pub execution_date: Option<NaiveDate>,
pub frozen_positions: BTreeMap<String, FrozenStockPoolPosition>,
pub prior_target_weights: BTreeMap<String, i32>,
/// Sizing ratios from prior plans; integer bps are display/legacy only.
pub prior_target_weight_ratios: BTreeMap<String, Decimal>,
pub pending_entry_symbols: BTreeSet<String>,
pub next_day_outside_exit_symbols: BTreeSet<String>,
pub market_timing_policy: Option<crate::stock_pool_index_policy::MarketTimingPolicy>,
@@ -530,6 +532,8 @@ pub struct StockPoolPlanRow {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StockPoolPlan {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub target_weight_ratios: BTreeMap<String, Decimal>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub position_action_bases: BTreeMap<String, Decimal>,
pub market_timing: Option<crate::stock_pool_index_policy::MarketTimingEvaluation>,
@@ -1096,6 +1100,11 @@ pub fn build_stock_pool_target_plan_with_fee_model(
target_count,
)?
};
let target_weight_ratios = frozen::sizing_ratios(
&original_final_symbols, &active_symbols, &explicit_weights,
constraints, reserved_protected_slots, &weights,
)?;
let sizing_ratio = |symbol: &str| target_weight_ratios.get(symbol).copied().unwrap_or(Decimal::ZERO);
for symbol in &rebuy_exclusions {
if original_final_symbols.contains(symbol) || current.contains_key(symbol) {
weights.insert(symbol.clone(), 0);
@@ -1235,8 +1244,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
.map(|symbol| {
let current_value = current[symbol].0
* frozen::valuation(symbol, &quote_map, &constraints.frozen_positions)?;
let desired =
budget * Decimal::from(*weights.get(symbol).unwrap_or(&0)) / Decimal::from(10_000);
let desired = budget * sizing_ratio(symbol);
if constraints.frozen_positions.contains_key(symbol) {
return Ok((symbol.clone(), desired));
}
@@ -1277,7 +1285,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
let free_desired = weights
.iter()
.filter(|(symbol, _)| !protected_values.contains_key(*symbol))
.map(|(_, weight)| budget * Decimal::from(*weight) / Decimal::from(10_000))
.map(|(symbol, _)| budget * sizing_ratio(symbol))
.sum::<Decimal>();
let free_budget = (budget * Decimal::from(requested_weight_total) / Decimal::from(10_000)
- protected_values.values().copied().sum::<Decimal>())
@@ -1296,7 +1304,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
rows.push(StockPoolPlanRow {
symbol: symbol.clone(),
target_weight_bps: weight,
target_value: budget * Decimal::from(weight) / Decimal::from(10_000),
target_value: budget * sizing_ratio(symbol),
current_quantity: quantity,
target_quantity: quantity,
delta_quantity: Decimal::ZERO,
@@ -1570,7 +1578,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
let target_value = protected_values
.get(symbol)
.copied()
.unwrap_or(budget * Decimal::from(weight) / Decimal::from(10_000) * free_scale)
.unwrap_or(if weight == 0 { Decimal::ZERO } else { budget * sizing_ratio(symbol) * free_scale })
.round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven);
let sizing_price = if target_value >= current_quantity * quote.last_price {
quote.buy_sizing_price.unwrap_or(quote.last_price)
@@ -1995,6 +2003,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
.map(|row| (row.symbol.clone(), constraints.position_action_bases.get(&row.symbol).copied().unwrap_or(row.current_quantity)))
.collect();
Ok(StockPoolPlan {
target_weight_ratios,
position_action_bases,
market_timing,
rows,