修正下一开盘目标仓位计算
This commit is contained in:
+103
-12
@@ -1728,6 +1728,7 @@ where
|
||||
daily_holdings: Vec::new(),
|
||||
metrics: BacktestMetrics::default(),
|
||||
};
|
||||
let mut stock_equity_by_date = BTreeMap::<NaiveDate, f64>::new();
|
||||
|
||||
for (execution_idx, execution_date) in execution_dates.iter().copied().enumerate() {
|
||||
let mut corporate_action_notes = Vec::new();
|
||||
@@ -1875,8 +1876,12 @@ where
|
||||
process_events: day_process_events,
|
||||
});
|
||||
result.process_events.append(&mut process_events);
|
||||
stock_equity_by_date.insert(execution_date, portfolio.total_equity());
|
||||
continue;
|
||||
};
|
||||
let decision_total_equity = (decision_date < execution_date)
|
||||
.then(|| stock_equity_by_date.get(&decision_date).copied())
|
||||
.flatten();
|
||||
let mut process_events = Vec::new();
|
||||
let mut directive_report = BrokerExecutionReport::default();
|
||||
let pre_open_orders = self.open_order_views();
|
||||
@@ -2073,10 +2078,11 @@ where
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
let mut report = self.broker.execute_with_event_dates(
|
||||
let mut report = self.broker.execute_with_event_dates_and_decision_equity(
|
||||
execution_date,
|
||||
decision_date,
|
||||
decision_date,
|
||||
decision_total_equity,
|
||||
&mut portfolio,
|
||||
&self.data,
|
||||
&auction_decision,
|
||||
@@ -2321,10 +2327,11 @@ where
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
let mut intraday_report = self.broker.execute_with_event_dates(
|
||||
let mut intraday_report = self.broker.execute_with_event_dates_and_decision_equity(
|
||||
execution_date,
|
||||
decision_date,
|
||||
decision_date,
|
||||
decision_total_equity,
|
||||
&mut portfolio,
|
||||
&self.data,
|
||||
&decision,
|
||||
@@ -2492,16 +2499,19 @@ where
|
||||
Some(minute_time),
|
||||
Some(minute_time),
|
||||
)?;
|
||||
let mut minute_report = self.broker.execute_between_with_event_dates(
|
||||
execution_date,
|
||||
decision_date,
|
||||
decision_date,
|
||||
&mut portfolio,
|
||||
&self.data,
|
||||
&minute_decision,
|
||||
Some(minute_time),
|
||||
Some(minute_time),
|
||||
)?;
|
||||
let mut minute_report = self
|
||||
.broker
|
||||
.execute_between_with_event_dates_and_decision_equity(
|
||||
execution_date,
|
||||
decision_date,
|
||||
decision_date,
|
||||
decision_total_equity,
|
||||
&mut portfolio,
|
||||
&self.data,
|
||||
&minute_decision,
|
||||
Some(minute_time),
|
||||
Some(minute_time),
|
||||
)?;
|
||||
let post_minute_open_orders = self.open_order_views();
|
||||
publish_process_events(
|
||||
&mut self.strategy,
|
||||
@@ -2888,6 +2898,7 @@ where
|
||||
process_events: day_process_events,
|
||||
});
|
||||
result.process_events.extend(process_events);
|
||||
stock_equity_by_date.insert(execution_date, portfolio.total_equity());
|
||||
}
|
||||
|
||||
if let Some(last_date) = execution_dates.last().copied() {
|
||||
@@ -4323,6 +4334,43 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ScheduledTargetPercentStrategy {
|
||||
first_decision_date: NaiveDate,
|
||||
second_decision_date: NaiveDate,
|
||||
}
|
||||
|
||||
impl Strategy for ScheduledTargetPercentStrategy {
|
||||
fn name(&self) -> &str {
|
||||
"scheduled_target_percent"
|
||||
}
|
||||
|
||||
fn on_day(
|
||||
&mut self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
) -> Result<StrategyDecision, super::BacktestError> {
|
||||
let order_intents = if ctx.decision_date == self.first_decision_date {
|
||||
vec![OrderIntent::Shares {
|
||||
symbol: SYMBOL.to_string(),
|
||||
quantity: 1_000,
|
||||
reason: "initial_position".to_string(),
|
||||
}]
|
||||
} else if ctx.decision_date == self.second_decision_date {
|
||||
vec![OrderIntent::TargetPercent {
|
||||
symbol: SYMBOL.to_string(),
|
||||
target_percent: 0.5,
|
||||
reason: "frozen_target_percent".to_string(),
|
||||
}]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
Ok(StrategyDecision {
|
||||
order_intents,
|
||||
..StrategyDecision::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ScheduledEligibleUniverseBuyStrategy {
|
||||
rule: ScheduleRule,
|
||||
@@ -5003,6 +5051,49 @@ mod tests {
|
||||
assert_eq!(result.fills[0].quantity, 8_300);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_bar_open_target_percent_freezes_decision_day_equity() {
|
||||
let first = d(2025, 1, 2);
|
||||
let second = d(2025, 1, 3);
|
||||
let third = d(2025, 1, 6);
|
||||
let dataset = dataset_from_market_and_candidates(
|
||||
vec![
|
||||
market(first, 10.0, 10.0),
|
||||
market(second, 10.0, 10.0),
|
||||
market(third, 20.0, 20.0),
|
||||
],
|
||||
vec![candidate(first), candidate(second), candidate(third)],
|
||||
);
|
||||
let config = BacktestConfig {
|
||||
initial_cash: 100_000.0,
|
||||
benchmark_code: "000852.SH".to_string(),
|
||||
start_date: Some(first),
|
||||
end_date: Some(third),
|
||||
decision_lag_trading_days: 1,
|
||||
execution_price_field: PriceField::Open,
|
||||
};
|
||||
|
||||
let result = BacktestEngine::new(
|
||||
dataset,
|
||||
ScheduledTargetPercentStrategy {
|
||||
first_decision_date: first,
|
||||
second_decision_date: second,
|
||||
},
|
||||
scheduled_next_open_broker(FidcRiskControlConfig::default()),
|
||||
config,
|
||||
)
|
||||
.run()
|
||||
.expect("backtest run");
|
||||
|
||||
assert_eq!(result.fills.len(), 2, "fills={:?}", result.fills);
|
||||
assert_eq!(result.fills[0].date, second);
|
||||
assert_eq!(result.fills[0].quantity, 1_000);
|
||||
assert_eq!(result.fills[1].date, third);
|
||||
assert_eq!(result.fills[1].price, 20.0);
|
||||
assert_eq!(result.fills[1].quantity, 1_400);
|
||||
assert_eq!(result.fills[1].decision_date, Some(second));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_bar_open_executes_last_decision_without_execution_day_factor_snapshot() {
|
||||
let first = d(2025, 1, 2);
|
||||
|
||||
Reference in New Issue
Block a user