按完整目标集合约束持仓槽位
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user