修复目标组合零权重估值

This commit is contained in:
boris
2026-06-29 14:59:20 +08:00
parent 49981f2f3e
commit fbc6da1a8f
2 changed files with 158 additions and 1 deletions
+61 -1
View File
@@ -1661,10 +1661,17 @@ where
) -> Result<(BTreeMap<String, u32>, Vec<String>), BacktestError> { ) -> Result<(BTreeMap<String, u32>, Vec<String>), BacktestError> {
let equity = let equity =
self.rebalance_total_equity_at_with_overrides(date, portfolio, data, valuation_prices)?; self.rebalance_total_equity_at_with_overrides(date, portfolio, data, valuation_prices)?;
let target_weight_sum = target_weights.values().copied().sum::<f64>(); let target_weight_sum = target_weights
.values()
.copied()
.filter(|weight| weight.abs() > f64::EPSILON)
.sum::<f64>();
let mut desired_targets = BTreeMap::new(); let mut desired_targets = BTreeMap::new();
let mut diagnostics = Vec::new(); let mut diagnostics = Vec::new();
for (symbol, weight) in target_weights { for (symbol, weight) in target_weights {
if weight.abs() <= f64::EPSILON {
continue;
}
let price = self.rebalance_valuation_price_with_overrides( let price = self.rebalance_valuation_price_with_overrides(
date, date,
symbol, symbol,
@@ -5544,6 +5551,59 @@ mod tests {
); );
} }
#[test]
fn target_portfolio_smart_ignores_zero_weight_symbols_without_market_snapshot() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Open,
)
.with_volume_limit(false)
.with_liquidity_limit(false);
let data = DataSet::from_components_with_actions_and_quotes(
vec![limit_test_instrument()],
vec![limit_test_snapshot()],
Vec::new(),
vec![limit_test_candidate(true, true)],
vec![limit_test_benchmark()],
Vec::new(),
Vec::new(),
)
.expect("valid dataset");
let mut portfolio = PortfolioState::new(1_000_000.0);
let mut target_weights = BTreeMap::new();
target_weights.insert("000001.SZ".to_string(), 0.50);
target_weights.insert("001239.SZ".to_string(), 0.0);
let mut report = BrokerExecutionReport::default();
broker
.process_target_portfolio_smart(
date,
&mut portfolio,
&data,
&target_weights,
None,
None,
"date_conditioned_target_weights",
&mut BTreeMap::new(),
&mut BTreeMap::new(),
&mut None,
&mut BTreeMap::new(),
&mut report,
)
.expect("zero weight missing snapshot symbol ignored");
assert!(portfolio.position("000001.SZ").is_some());
assert!(portfolio.position("001239.SZ").is_none());
assert!(
report
.fill_events
.iter()
.all(|event| event.symbol != "001239.SZ")
);
}
#[test] #[test]
fn timed_target_value_zero_sells_full_position_at_requested_time_quote() { fn timed_target_value_zero_sells_full_position_at_requested_time_quote() {
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");
@@ -20192,6 +20192,103 @@ mod tests {
} }
} }
#[test]
fn target_portfolio_smart_respects_current_date_weight_conditions() {
let date = d(2023, 1, 3);
let data = DataSet::from_components(
vec![],
vec![DailyMarketSnapshot {
date,
symbol: "000001.SH".to_string(),
timestamp: Some("2023-01-03 10:18:00".to_string()),
day_open: 1000.0,
open: 1000.0,
high: 1002.0,
low: 998.0,
close: 1001.0,
last_price: 1001.0,
bid1: 1000.5,
ask1: 1001.5,
prev_close: 999.0,
volume: 100_000,
minute_volume: 5_000,
bid1_volume: 2_500,
ask1_volume: 2_500,
trading_phase: Some("continuous".to_string()),
paused: false,
upper_limit: 1098.9,
lower_limit: 899.1,
price_tick: 0.01,
}],
vec![],
vec![],
vec![BenchmarkSnapshot {
date,
benchmark: "000852.SH".to_string(),
open: 1000.0,
close: 1001.0,
prev_close: 999.0,
volume: 100_000,
}],
)
.expect("dataset");
let portfolio = PortfolioState::new(1_000_000.0);
let subscriptions = BTreeSet::new();
let ctx = StrategyContext {
execution_date: date,
decision_date: date,
decision_index: 0,
data: &data,
portfolio: &portfolio,
futures_account: None,
open_orders: &[],
dynamic_universe: None,
subscriptions: &subscriptions,
process_events: &[],
active_process_event: None,
active_datetime: None,
order_events: &[],
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.signal_symbol = "000001.SH".to_string();
cfg.rotation_enabled = false;
cfg.benchmark_short_ma_days = 1;
cfg.benchmark_long_ma_days = 1;
cfg.explicit_actions = vec![PlatformTradeAction::TargetPortfolioSmart {
target_weights_expr: concat!(
"{",
"\"000521.SZ\": if current_date == \"2023-01-03\" { 0.025 } else { 0.0 },",
"\"001239.SZ\": if current_date == \"2024-08-13\" { 0.025 } else { 0.0 }",
"}"
)
.to_string(),
order_prices_expr: None,
valuation_prices_expr: None,
when_expr: None,
reason: "date_conditioned_target_weights".to_string(),
}];
let mut strategy = PlatformExprStrategy::new(cfg);
let decision = strategy.on_day(&ctx).expect("platform decision");
assert_eq!(decision.order_intents.len(), 1);
match &decision.order_intents[0] {
crate::strategy::OrderIntent::TargetPortfolioSmart { target_weights, .. } => {
assert_eq!(target_weights.get("000521.SZ").copied(), Some(0.025));
assert_eq!(target_weights.get("001239.SZ").copied(), Some(0.0));
assert_eq!(
target_weights
.iter()
.filter(|(_, weight)| weight.abs() > f64::EPSILON)
.count(),
1
);
}
other => panic!("unexpected explicit target portfolio intent: {other:?}"),
}
}
#[test] #[test]
fn platform_strategy_emits_target_portfolio_smart_algo_order_style() { fn platform_strategy_emits_target_portfolio_smart_algo_order_style() {
let date = d(2025, 2, 3); let date = d(2025, 2, 3);