修正持久目标组合调仓顺序
This commit is contained in:
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user