修正持久目标组合调仓顺序

This commit is contained in:
boris
2026-07-17 22:41:03 +08:00
parent 031e4ee054
commit 71b5acee30
+174 -18
View File
@@ -9659,6 +9659,9 @@ impl Strategy for PlatformExprStrategy {
&& trading_ratio > 0.0 && trading_ratio > 0.0
&& (self.config.target_portfolio_daily_enabled || trading_ratio < 1.0) && (self.config.target_portfolio_daily_enabled || trading_ratio < 1.0)
&& selection_limit > 0 && selection_limit > 0
&& !(persistent_model_lifecycle
&& self.config.target_portfolio_daily_enabled
&& daily_top_up_active)
&& (!ctx.portfolio.positions().is_empty() && (!ctx.portfolio.positions().is_empty()
|| (persistent_model_lifecycle && !self.position_entry_dates.is_empty())) || (persistent_model_lifecycle && !self.position_entry_dates.is_empty()))
{ {
@@ -10188,7 +10191,89 @@ impl Strategy for PlatformExprStrategy {
} }
} }
if daily_top_up_active && self.config.target_portfolio_daily_enabled { if daily_top_up_active
&& self.config.target_portfolio_daily_enabled
&& persistent_model_lifecycle
{
let mut target_symbols = self
.position_entry_dates
.keys()
.filter(|symbol| !exit_symbols.contains(*symbol))
.cloned()
.collect::<BTreeSet<_>>();
for symbol in &stock_list {
if target_symbols.len() >= selection_limit {
break;
}
if target_symbols.contains(symbol) || exit_symbols.contains(symbol) {
continue;
}
if ctx
.data
.market_latest_back_adjusted_close(signal_date, symbol)
.is_none()
{
continue;
}
self.remember_position_entry_date(symbol, signal_date);
target_symbols.insert(symbol.clone());
}
for symbol in target_symbols {
let decision_stock = self.stock_state_with_factor_date(
ctx,
decision_date,
selection_factor_date,
&symbol,
)?;
let stock_scale = self.buy_scale(ctx, &day, &decision_stock)?;
let target_value =
aiquant_total_value * trading_ratio / selection_limit as f64 * stock_scale;
if !target_value.is_finite() || target_value <= 0.0 {
continue;
}
let before_qty = projected
.position(&symbol)
.map(|position| position.quantity)
.unwrap_or(0);
if before_qty > 0 {
self.project_target_value(
ctx,
&mut projected,
projection_date,
&symbol,
target_value,
&mut projected_execution_state,
);
} else {
self.project_order_value(
ctx,
&mut projected,
projection_date,
&symbol,
target_value,
&mut projected_execution_state,
);
intraday_attempted_buys.insert(symbol.clone());
}
let after_qty = projected
.position(&symbol)
.map(|position| position.quantity)
.unwrap_or(0);
order_intents.push(OrderIntent::TargetValue {
symbol: symbol.clone(),
target_value,
reason: "model_target_portfolio_daily".to_string(),
});
if defer_execution_risk {
deferred_daily_target_values.insert(symbol.clone(), target_value);
}
if after_qty > before_qty {
same_bar_buy_symbols.insert(symbol);
}
}
} else if daily_top_up_active && self.config.target_portfolio_daily_enabled {
self.try_daily_top_up_at_position( self.try_daily_top_up_at_position(
ctx, ctx,
&day, &day,
@@ -23892,9 +23977,30 @@ mod tests {
status: "active".to_string(), status: "active".to_string(),
}) })
.collect(), .collect(),
symbols std::iter::once(DailyMarketSnapshot {
.iter() date: prev_date,
.map(|symbol| DailyMarketSnapshot { symbol: take_profit.to_string(),
timestamp: Some("2025-04-29 15:00:00".to_string()),
day_open: 8.0,
open: 8.0,
high: 8.0,
low: 8.0,
close: 8.0,
last_price: 8.0,
bid1: 8.0,
ask1: 8.0,
prev_close: 8.0,
volume: 1_000_000,
minute_volume: 20_000,
bid1_volume: 20_000,
ask1_volume: 20_000,
trading_phase: Some("closed".to_string()),
paused: false,
upper_limit: 8.8,
lower_limit: 7.2,
price_tick: 0.01,
})
.chain(symbols.iter().map(|symbol| DailyMarketSnapshot {
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: Some("2025-04-30 10:18:00".to_string()), timestamp: Some("2025-04-30 10:18:00".to_string()),
@@ -23916,11 +24022,19 @@ mod tests {
upper_limit: 11.0, upper_limit: 11.0,
lower_limit: 9.0, lower_limit: 9.0,
price_tick: 0.01, price_tick: 0.01,
}) }))
.collect(), .collect(),
symbols std::iter::once(DailyFactorSnapshot {
.iter() date: prev_date,
.map(|symbol| DailyFactorSnapshot { symbol: take_profit.to_string(),
market_cap_bn: 100.0,
free_float_cap_bn: 10.0,
pe_ttm: 8.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
extra_factors: BTreeMap::from([("adjustment_factor_backward1".to_string(), 1.0)]),
})
.chain(symbols.iter().map(|symbol| DailyFactorSnapshot {
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
market_cap_bn: match *symbol { market_cap_bn: match *symbol {
@@ -23934,8 +24048,8 @@ mod tests {
pe_ttm: 8.0, pe_ttm: 8.0,
turnover_ratio: Some(1.0), turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0), effective_turnover_ratio: Some(1.0),
extra_factors: BTreeMap::new(), extra_factors: BTreeMap::from([("adjustment_factor_backward1".to_string(), 1.0)]),
}) }))
.collect(), .collect(),
symbols symbols
.iter() .iter()
@@ -24023,22 +24137,31 @@ mod tests {
cfg.daily_top_up_enabled = true; cfg.daily_top_up_enabled = true;
cfg.daily_position_target_adjust_enabled = true; cfg.daily_position_target_adjust_enabled = true;
cfg.target_portfolio_daily_enabled = true; cfg.target_portfolio_daily_enabled = true;
cfg.hold_until_exit_enabled = true;
cfg.stop_take_reference_price_mode =
PlatformStopTakeReferencePriceMode::SignalDayPostAdjustedClose;
cfg.release_slot_on_exit_signal = true; cfg.release_slot_on_exit_signal = true;
cfg.aiquant_transaction_cost = true; cfg.aiquant_transaction_cost = true;
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()); cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap());
let mut strategy = PlatformExprStrategy::new(cfg); let mut strategy = PlatformExprStrategy::new(cfg);
strategy.rebalance_day_counter = 2; strategy.rebalance_day_counter = 2;
strategy.last_rebalance_date = Some(prev_date);
strategy
.position_entry_dates
.insert(take_profit.to_string(), prev_date);
let decision = strategy.on_day(&ctx).expect("platform decision"); let decision = strategy.on_day(&ctx).expect("platform decision");
let top_ups = decision let positive_targets = decision
.order_intents .order_intents
.iter() .iter()
.filter_map(|intent| match intent { .filter_map(|intent| match intent {
OrderIntent::Value { OrderIntent::TargetValue {
symbol, symbol,
value, target_value,
reason, reason,
} if reason == "daily_top_up_buy" => Some((symbol.as_str(), *value)), } if reason == "model_target_portfolio_daily" && *target_value > 0.0 => {
Some((symbol.as_str(), *target_value))
}
_ => None, _ => None,
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -24055,18 +24178,51 @@ mod tests {
"{:?}", "{:?}",
decision.order_intents decision.order_intents
); );
let clear_index = decision
.order_intents
.iter()
.position(|intent| matches!(
intent,
OrderIntent::TargetValue {
symbol,
target_value,
reason,
} if symbol == take_profit && *target_value == 0.0 && reason == "take_profit_exit"
))
.expect("take-profit clear intent");
let first_positive_index = decision
.order_intents
.iter()
.position(|intent| {
matches!(
intent,
OrderIntent::TargetValue {
target_value,
reason,
..
} if *target_value > 0.0 && reason == "model_target_portfolio_daily"
)
})
.expect("positive target intent");
assert!(
clear_index < first_positive_index,
"{:?}",
decision.order_intents
);
assert_eq!( assert_eq!(
top_ups positive_targets
.iter() .iter()
.map(|(symbol, _)| *symbol) .map(|(symbol, _)| *symbol)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
vec![buy_first, buy_second], vec![buy_first, buy_second, keep_first, keep_second],
"{:?}", "{:?}",
decision.order_intents decision.order_intents
); );
assert!( assert!(
top_ups.iter().all(|(_, value)| *value >= 6_900.0), positive_targets
"top ups must be sized after all same-batch sells: {:?}", .iter()
.all(|(_, target_value)| (*target_value - 7_500.0).abs() < 1e-9),
"all targets must use the frozen target allocation: {:?}",
decision.order_intents decision.order_intents
); );
} }