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

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");
+2 -19
View File
@@ -13295,7 +13295,7 @@ impl Strategy for PlatformExprStrategy {
{
continue;
}
let execution_stock = if self.uses_intraday_execution_quotes() {
if self.uses_intraday_execution_quotes() {
let execution_time = self.intraday_execution_start_time();
if self
.scheduled_quote_at_time(ctx, projection_date, symbol, Some(execution_time))
@@ -13306,10 +13306,7 @@ impl Strategy for PlatformExprStrategy {
));
continue;
}
self.stock_state_at_time(ctx, projection_date, symbol, Some(execution_time))?
} else {
self.stock_state(ctx, projection_date, symbol)?
};
}
let target_value = target_value_for_scale(
strategy_visible_total_value,
trading_ratio,
@@ -13324,20 +13321,6 @@ impl Strategy for PlatformExprStrategy {
.position(&symbol)
.map(|position| position.quantity)
.unwrap_or(0);
if before_qty == 0
&& !defer_execution_risk
&& let Some(reason) = self.buy_rejection_reason(
ctx,
execution_date,
symbol,
execution_stock.as_ref(),
)?
{
selection_notes.push(format!(
"daily_target_skipped symbol={symbol} reason={reason} no_order=true"
));
continue;
}
if before_qty > 0 {
self.project_target_value(
ctx,