按完整目标集合约束持仓槽位

This commit is contained in:
boris
2026-09-07 05:18:57 +08:00
parent e4f6cdd025
commit e00777ebc2
2 changed files with 79 additions and 19 deletions
+77
View File
@@ -966,6 +966,31 @@ where
}
}
fn complete_daily_target_position(intent: &OrderIntent) -> Option<(&str, bool)> {
match intent.unwrapped() {
OrderIntent::TargetValue {
symbol,
target_value,
reason,
}
| OrderIntent::TimedTargetValue {
symbol,
target_value,
reason,
..
}
| OrderIntent::LimitTargetValue {
symbol,
target_value,
reason,
..
} if reason == "model_target_portfolio_daily" => {
Some((symbol, target_value.is_finite() && *target_value > 0.0))
}
_ => None,
}
}
fn infer_target_position_limit(
&self,
portfolio: &PortfolioState,
@@ -979,6 +1004,21 @@ where
return None;
}
let complete_daily_targets = intents
.iter()
.filter_map(|intent| Self::complete_daily_target_position(intent))
.collect::<Vec<_>>();
if !complete_daily_targets.is_empty() {
return Some(
complete_daily_targets
.into_iter()
.filter(|(_, positive)| *positive)
.map(|(symbol, _)| symbol)
.collect::<BTreeSet<_>>()
.len(),
);
}
let held_symbols = portfolio
.positions()
.iter()
@@ -10206,6 +10246,43 @@ mod tests {
}
}
#[test]
fn complete_daily_target_batch_uses_positive_target_count_for_slot_limit() {
let prev_date = chrono::NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date");
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Open,
);
let mut portfolio = PortfolioState::new(20_000.0);
for symbol in ["000001.SZ", "000002.SZ", "000003.SZ"] {
portfolio.position_mut(symbol).buy(prev_date, 1_000, 10.0);
}
let intents = vec![
OrderIntent::TargetValue {
symbol: "000001.SZ".to_string(),
target_value: 0.0,
reason: "stop_loss_exit".to_string(),
},
OrderIntent::TargetValue {
symbol: "000002.SZ".to_string(),
target_value: 10_000.0,
reason: "model_target_portfolio_daily".to_string(),
},
OrderIntent::TargetValue {
symbol: "000004.SZ".to_string(),
target_value: 10_000.0,
reason: "model_target_portfolio_daily".to_string(),
},
];
let refs = intents.iter().collect::<Vec<_>>();
assert_eq!(
broker.infer_target_position_limit(&portfolio, &refs),
Some(2)
);
}
#[test]
fn failed_target_exit_blocks_replacement_entry_when_no_position_slot_is_released() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");