按完整目标集合约束持仓槽位
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(
|
fn infer_target_position_limit(
|
||||||
&self,
|
&self,
|
||||||
portfolio: &PortfolioState,
|
portfolio: &PortfolioState,
|
||||||
@@ -979,6 +1004,21 @@ where
|
|||||||
return None;
|
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
|
let held_symbols = portfolio
|
||||||
.positions()
|
.positions()
|
||||||
.iter()
|
.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]
|
#[test]
|
||||||
fn failed_target_exit_blocks_replacement_entry_when_no_position_slot_is_released() {
|
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");
|
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||||
|
|||||||
@@ -13295,7 +13295,7 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let execution_stock = if self.uses_intraday_execution_quotes() {
|
if self.uses_intraday_execution_quotes() {
|
||||||
let execution_time = self.intraday_execution_start_time();
|
let execution_time = self.intraday_execution_start_time();
|
||||||
if self
|
if self
|
||||||
.scheduled_quote_at_time(ctx, projection_date, symbol, Some(execution_time))
|
.scheduled_quote_at_time(ctx, projection_date, symbol, Some(execution_time))
|
||||||
@@ -13306,10 +13306,7 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
));
|
));
|
||||||
continue;
|
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(
|
let target_value = target_value_for_scale(
|
||||||
strategy_visible_total_value,
|
strategy_visible_total_value,
|
||||||
trading_ratio,
|
trading_ratio,
|
||||||
@@ -13324,20 +13321,6 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
.position(&symbol)
|
.position(&symbol)
|
||||||
.map(|position| position.quantity)
|
.map(|position| position.quantity)
|
||||||
.unwrap_or(0);
|
.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 {
|
if before_qty > 0 {
|
||||||
self.project_target_value(
|
self.project_target_value(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
Reference in New Issue
Block a user