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