修正下一开盘目标仓位计算
This commit is contained in:
+330
-46
@@ -64,6 +64,7 @@ struct TargetConstraint {
|
||||
desired_qty: u32,
|
||||
provisional_target_qty: u32,
|
||||
price: f64,
|
||||
buy_execution_price: f64,
|
||||
minimum_order_quantity: u32,
|
||||
order_step_size: u32,
|
||||
}
|
||||
@@ -193,6 +194,7 @@ pub struct BrokerSimulator<C, R> {
|
||||
runtime_intraday_end_time: Cell<Option<NaiveTime>>,
|
||||
runtime_decision_date: Cell<Option<NaiveDate>>,
|
||||
runtime_order_created_date: Cell<Option<NaiveDate>>,
|
||||
runtime_decision_total_equity: Cell<Option<f64>>,
|
||||
next_order_id: Cell<u64>,
|
||||
open_orders: RefCell<Vec<OpenOrder>>,
|
||||
}
|
||||
@@ -210,7 +212,7 @@ impl<C, R> BrokerSimulator<C, R> {
|
||||
volume_limit: true,
|
||||
inactive_limit: true,
|
||||
liquidity_limit: true,
|
||||
strict_value_budget: false,
|
||||
strict_value_budget: true,
|
||||
rebalance_cash_mode: RebalanceCashMode::default(),
|
||||
sell_then_buy_delay_slippage_rate: 0.0,
|
||||
aiquant_execution_rules: false,
|
||||
@@ -222,6 +224,7 @@ impl<C, R> BrokerSimulator<C, R> {
|
||||
runtime_intraday_end_time: Cell::new(None),
|
||||
runtime_decision_date: Cell::new(None),
|
||||
runtime_order_created_date: Cell::new(None),
|
||||
runtime_decision_total_equity: Cell::new(None),
|
||||
next_order_id: Cell::new(1),
|
||||
open_orders: RefCell::new(Vec::new()),
|
||||
}
|
||||
@@ -243,7 +246,7 @@ impl<C, R> BrokerSimulator<C, R> {
|
||||
volume_limit: true,
|
||||
inactive_limit: true,
|
||||
liquidity_limit: true,
|
||||
strict_value_budget: false,
|
||||
strict_value_budget: true,
|
||||
rebalance_cash_mode: RebalanceCashMode::default(),
|
||||
sell_then_buy_delay_slippage_rate: 0.0,
|
||||
aiquant_execution_rules: false,
|
||||
@@ -255,6 +258,7 @@ impl<C, R> BrokerSimulator<C, R> {
|
||||
runtime_intraday_end_time: Cell::new(None),
|
||||
runtime_decision_date: Cell::new(None),
|
||||
runtime_order_created_date: Cell::new(None),
|
||||
runtime_decision_total_equity: Cell::new(None),
|
||||
next_order_id: Cell::new(1),
|
||||
open_orders: RefCell::new(Vec::new()),
|
||||
}
|
||||
@@ -276,7 +280,11 @@ impl<C, R> BrokerSimulator<C, R> {
|
||||
}
|
||||
|
||||
pub fn with_strict_value_budget(mut self, enabled: bool) -> Self {
|
||||
self.strict_value_budget = enabled;
|
||||
assert!(
|
||||
enabled,
|
||||
"strict value budget is mandatory for FIDC order sizing"
|
||||
);
|
||||
self.strict_value_budget = true;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -427,11 +435,11 @@ where
|
||||
symbol: &str,
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
) -> f64 {
|
||||
if self.matching_type == MatchingType::NextBarOpen
|
||||
&& snapshot.prev_close.is_finite()
|
||||
&& snapshot.prev_close > 0.0
|
||||
{
|
||||
return snapshot.prev_close;
|
||||
if self.matching_type == MatchingType::NextBarOpen {
|
||||
let execution_price = snapshot.price(PriceField::Open);
|
||||
if execution_price.is_finite() && execution_price > 0.0 {
|
||||
return execution_price;
|
||||
}
|
||||
}
|
||||
if self.aiquant_execution_rules && self.execution_price_field == PriceField::Last {
|
||||
let start_cursor = self
|
||||
@@ -716,16 +724,42 @@ where
|
||||
portfolio: &mut PortfolioState,
|
||||
data: &DataSet,
|
||||
decision: &StrategyDecision,
|
||||
) -> Result<BrokerExecutionReport, BacktestError> {
|
||||
self.execute_with_event_dates_and_decision_equity(
|
||||
date,
|
||||
decision_date,
|
||||
order_created_date,
|
||||
None,
|
||||
portfolio,
|
||||
data,
|
||||
decision,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn execute_with_event_dates_and_decision_equity(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
decision_date: NaiveDate,
|
||||
order_created_date: NaiveDate,
|
||||
decision_total_equity: Option<f64>,
|
||||
portfolio: &mut PortfolioState,
|
||||
data: &DataSet,
|
||||
decision: &StrategyDecision,
|
||||
) -> Result<BrokerExecutionReport, BacktestError> {
|
||||
let previous_decision_date = self.runtime_decision_date.get();
|
||||
let previous_order_created_date = self.runtime_order_created_date.get();
|
||||
let previous_decision_total_equity = self.runtime_decision_total_equity.get();
|
||||
self.runtime_decision_date.set(Some(decision_date));
|
||||
self.runtime_order_created_date
|
||||
.set(Some(order_created_date));
|
||||
self.runtime_decision_total_equity
|
||||
.set(decision_total_equity.filter(|equity| equity.is_finite() && *equity >= 0.0));
|
||||
let result = self.execute_with_runtime_dates(date, portfolio, data, decision);
|
||||
self.runtime_decision_date.set(previous_decision_date);
|
||||
self.runtime_order_created_date
|
||||
.set(previous_order_created_date);
|
||||
self.runtime_decision_total_equity
|
||||
.set(previous_decision_total_equity);
|
||||
result
|
||||
}
|
||||
|
||||
@@ -880,15 +914,42 @@ where
|
||||
decision: &StrategyDecision,
|
||||
start_time: Option<NaiveTime>,
|
||||
end_time: Option<NaiveTime>,
|
||||
) -> Result<BrokerExecutionReport, BacktestError> {
|
||||
self.execute_between_with_event_dates_and_decision_equity(
|
||||
date,
|
||||
decision_date,
|
||||
order_created_date,
|
||||
None,
|
||||
portfolio,
|
||||
data,
|
||||
decision,
|
||||
start_time,
|
||||
end_time,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn execute_between_with_event_dates_and_decision_equity(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
decision_date: NaiveDate,
|
||||
order_created_date: NaiveDate,
|
||||
decision_total_equity: Option<f64>,
|
||||
portfolio: &mut PortfolioState,
|
||||
data: &DataSet,
|
||||
decision: &StrategyDecision,
|
||||
start_time: Option<NaiveTime>,
|
||||
end_time: Option<NaiveTime>,
|
||||
) -> Result<BrokerExecutionReport, BacktestError> {
|
||||
let previous_start_time = self.runtime_intraday_start_time.get();
|
||||
let previous_end_time = self.runtime_intraday_end_time.get();
|
||||
self.runtime_intraday_start_time.set(start_time);
|
||||
self.runtime_intraday_end_time.set(end_time);
|
||||
let result = self.execute_with_event_dates(
|
||||
let result = self.execute_with_event_dates_and_decision_equity(
|
||||
date,
|
||||
decision_date,
|
||||
order_created_date,
|
||||
decision_total_equity,
|
||||
portfolio,
|
||||
data,
|
||||
decision,
|
||||
@@ -2017,8 +2078,22 @@ where
|
||||
target_weights: &BTreeMap<String, f64>,
|
||||
valuation_prices: Option<&BTreeMap<String, f64>>,
|
||||
) -> Result<(BTreeMap<String, u32>, Vec<String>), BacktestError> {
|
||||
let equity =
|
||||
self.rebalance_total_equity_at_with_overrides(date, portfolio, data, valuation_prices)?;
|
||||
let equity = if valuation_prices.is_none() {
|
||||
self.target_total_equity_at(date, portfolio, data)?
|
||||
} else {
|
||||
self.runtime_decision_total_equity
|
||||
.get()
|
||||
.filter(|equity| equity.is_finite() && *equity >= 0.0)
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| {
|
||||
self.rebalance_total_equity_at_with_overrides(
|
||||
date,
|
||||
portfolio,
|
||||
data,
|
||||
valuation_prices,
|
||||
)
|
||||
})?
|
||||
};
|
||||
let target_weight_sum = target_weights
|
||||
.values()
|
||||
.copied()
|
||||
@@ -2036,15 +2111,33 @@ where
|
||||
data,
|
||||
valuation_prices,
|
||||
)?;
|
||||
let raw_qty = ((equity * weight) / price).floor() as u32;
|
||||
desired_targets.insert(
|
||||
symbol.clone(),
|
||||
let current_qty = portfolio
|
||||
.position(symbol)
|
||||
.map(|position| position.quantity)
|
||||
.unwrap_or(0);
|
||||
let target_value = (equity * weight).max(0.0);
|
||||
let current_value = price * current_qty as f64;
|
||||
let minimum_order_quantity = self.minimum_order_quantity(data, symbol);
|
||||
let order_step_size = self.order_step_size(data, symbol);
|
||||
let desired_qty = if target_value > current_value + f64::EPSILON {
|
||||
let buy_budget = target_value - current_value;
|
||||
current_qty.saturating_add(self.target_buy_quantity_for_budget(
|
||||
date,
|
||||
data,
|
||||
symbol,
|
||||
buy_budget,
|
||||
price,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
))
|
||||
} else {
|
||||
self.round_buy_quantity(
|
||||
raw_qty,
|
||||
self.minimum_order_quantity(data, symbol),
|
||||
self.order_step_size(data, symbol),
|
||||
),
|
||||
);
|
||||
(target_value / price).floor() as u32,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
)
|
||||
};
|
||||
desired_targets.insert(symbol.clone(), desired_qty);
|
||||
}
|
||||
|
||||
let mut symbols = BTreeSet::new();
|
||||
@@ -2087,6 +2180,22 @@ where
|
||||
order_step_size,
|
||||
);
|
||||
let provisional_target_qty = desired_qty.clamp(min_target_qty, max_target_qty);
|
||||
let buy_quantity = provisional_target_qty.saturating_sub(current_qty);
|
||||
let sell_quantity = current_qty.saturating_sub(provisional_target_qty);
|
||||
let buy_execution_price = data
|
||||
.market(date, &symbol)
|
||||
.map(|snapshot| {
|
||||
self.snapshot_execution_price(snapshot, OrderSide::Buy, Some(buy_quantity))
|
||||
})
|
||||
.filter(|execution_price| execution_price.is_finite() && *execution_price > 0.0)
|
||||
.unwrap_or(price);
|
||||
let sell_execution_price = data
|
||||
.market(date, &symbol)
|
||||
.map(|snapshot| {
|
||||
self.snapshot_execution_price(snapshot, OrderSide::Sell, Some(sell_quantity))
|
||||
})
|
||||
.filter(|execution_price| execution_price.is_finite() && *execution_price > 0.0)
|
||||
.unwrap_or(price);
|
||||
if desired_qty < current_qty
|
||||
&& min_target_qty >= current_qty
|
||||
&& diagnostics.len() < 16
|
||||
@@ -2132,7 +2241,7 @@ where
|
||||
if current_qty > provisional_target_qty && cash_mode != RebalanceCashMode::PreOpenCash {
|
||||
projected_cash += self.estimated_sell_net_cash(
|
||||
date,
|
||||
price,
|
||||
sell_execution_price,
|
||||
current_qty.saturating_sub(provisional_target_qty),
|
||||
);
|
||||
}
|
||||
@@ -2142,6 +2251,7 @@ where
|
||||
desired_qty,
|
||||
provisional_target_qty,
|
||||
price,
|
||||
buy_execution_price,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
});
|
||||
@@ -2186,7 +2296,7 @@ where
|
||||
if target_qty > constraint.current_qty {
|
||||
buy_cash_out += self.estimated_buy_cash_out(
|
||||
date,
|
||||
constraint.price,
|
||||
constraint.buy_execution_price,
|
||||
target_qty - constraint.current_qty,
|
||||
);
|
||||
}
|
||||
@@ -2445,8 +2555,22 @@ where
|
||||
reason: &str,
|
||||
report: &mut BrokerExecutionReport,
|
||||
) -> Result<(), BacktestError> {
|
||||
let equity =
|
||||
self.rebalance_total_equity_at_with_overrides(date, portfolio, data, valuation_prices)?;
|
||||
let equity = if valuation_prices.is_none() {
|
||||
self.target_total_equity_at(date, portfolio, data)?
|
||||
} else {
|
||||
self.runtime_decision_total_equity
|
||||
.get()
|
||||
.filter(|equity| equity.is_finite() && *equity >= 0.0)
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| {
|
||||
self.rebalance_total_equity_at_with_overrides(
|
||||
date,
|
||||
portfolio,
|
||||
data,
|
||||
valuation_prices,
|
||||
)
|
||||
})?
|
||||
};
|
||||
for (symbol, weight) in target_weights {
|
||||
if weight.abs() <= f64::EPSILON {
|
||||
continue;
|
||||
@@ -4054,7 +4178,7 @@ where
|
||||
commission_state: &mut BTreeMap<u64, f64>,
|
||||
report: &mut BrokerExecutionReport,
|
||||
) -> Result<(), BacktestError> {
|
||||
let total_equity = self.rebalance_total_equity_at(date, portfolio, data)?;
|
||||
let total_equity = self.target_total_equity_at(date, portfolio, data)?;
|
||||
self.process_target_value(
|
||||
date,
|
||||
portfolio,
|
||||
@@ -4085,7 +4209,7 @@ where
|
||||
commission_state: &mut BTreeMap<u64, f64>,
|
||||
report: &mut BrokerExecutionReport,
|
||||
) -> Result<(), BacktestError> {
|
||||
let total_equity = self.rebalance_total_equity_at(date, portfolio, data)?;
|
||||
let total_equity = self.target_total_equity_at(date, portfolio, data)?;
|
||||
self.process_limit_target_value(
|
||||
date,
|
||||
portfolio,
|
||||
@@ -4324,7 +4448,7 @@ where
|
||||
commission_state: &mut BTreeMap<u64, f64>,
|
||||
report: &mut BrokerExecutionReport,
|
||||
) -> Result<(), BacktestError> {
|
||||
let total_equity = self.rebalance_total_equity_at(date, portfolio, data)?;
|
||||
let total_equity = self.target_total_equity_at(date, portfolio, data)?;
|
||||
self.process_value(
|
||||
date,
|
||||
portfolio,
|
||||
@@ -4355,7 +4479,7 @@ where
|
||||
commission_state: &mut BTreeMap<u64, f64>,
|
||||
report: &mut BrokerExecutionReport,
|
||||
) -> Result<(), BacktestError> {
|
||||
let total_equity = self.rebalance_total_equity_at(date, portfolio, data)?;
|
||||
let total_equity = self.target_total_equity_at(date, portfolio, data)?;
|
||||
self.process_limit_value(
|
||||
date,
|
||||
portfolio,
|
||||
@@ -4495,7 +4619,7 @@ where
|
||||
commission_state: &mut BTreeMap<u64, f64>,
|
||||
report: &mut BrokerExecutionReport,
|
||||
) -> Result<(), BacktestError> {
|
||||
let total_equity = self.rebalance_total_equity_at(date, portfolio, data)?;
|
||||
let total_equity = self.target_total_equity_at(date, portfolio, data)?;
|
||||
self.process_algo_value(
|
||||
date,
|
||||
portfolio,
|
||||
@@ -5343,6 +5467,22 @@ where
|
||||
self.rebalance_total_equity_at_with_overrides(date, portfolio, data, None)
|
||||
}
|
||||
|
||||
fn target_total_equity_at(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
portfolio: &PortfolioState,
|
||||
data: &DataSet,
|
||||
) -> Result<f64, BacktestError> {
|
||||
if let Some(equity) = self
|
||||
.runtime_decision_total_equity
|
||||
.get()
|
||||
.filter(|equity| equity.is_finite() && *equity >= 0.0)
|
||||
{
|
||||
return Ok(equity);
|
||||
}
|
||||
self.rebalance_total_equity_at(date, portfolio, data)
|
||||
}
|
||||
|
||||
fn rebalance_total_equity_at_with_overrides(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
@@ -5443,6 +5583,60 @@ where
|
||||
0
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn target_buy_quantity_for_budget(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
data: &DataSet,
|
||||
symbol: &str,
|
||||
value_budget: f64,
|
||||
fallback_price: f64,
|
||||
minimum_order_quantity: u32,
|
||||
order_step_size: u32,
|
||||
) -> u32 {
|
||||
let snapshot = data.market(date, symbol);
|
||||
let mut quantity = self.value_buy_quantity(
|
||||
date,
|
||||
value_budget,
|
||||
fallback_price,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
);
|
||||
for _ in 0..8 {
|
||||
let execution_price = snapshot
|
||||
.map(|snapshot| {
|
||||
self.snapshot_execution_price(snapshot, OrderSide::Buy, Some(quantity))
|
||||
})
|
||||
.filter(|price| price.is_finite() && *price > 0.0)
|
||||
.unwrap_or(fallback_price);
|
||||
let resolved = self.value_buy_quantity(
|
||||
date,
|
||||
value_budget,
|
||||
execution_price,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
);
|
||||
if resolved == quantity {
|
||||
return quantity;
|
||||
}
|
||||
quantity = resolved;
|
||||
}
|
||||
while quantity >= minimum_order_quantity.max(1) {
|
||||
let execution_price = snapshot
|
||||
.map(|snapshot| {
|
||||
self.snapshot_execution_price(snapshot, OrderSide::Buy, Some(quantity))
|
||||
})
|
||||
.filter(|price| price.is_finite() && *price > 0.0)
|
||||
.unwrap_or(fallback_price);
|
||||
if self.estimated_buy_cash_out(date, execution_price, quantity) <= value_budget + 1e-6 {
|
||||
return quantity;
|
||||
}
|
||||
quantity =
|
||||
self.decrement_order_quantity(quantity, minimum_order_quantity, order_step_size);
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn decrement_order_quantity(
|
||||
&self,
|
||||
quantity: u32,
|
||||
@@ -7154,7 +7348,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_open_target_value_valuation_uses_previous_close() {
|
||||
fn next_open_target_value_valuation_uses_execution_open() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
@@ -7181,10 +7375,68 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
broker.target_value_valuation_price(date, &data, "000001.SZ", snapshot),
|
||||
10.0
|
||||
11.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_open_target_value_recomputes_quantity_from_execution_open() {
|
||||
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_matching_type(MatchingType::NextBarOpen)
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false)
|
||||
.with_inactive_limit(false);
|
||||
let mut snapshot = limit_test_snapshot();
|
||||
snapshot.date = date;
|
||||
snapshot.prev_close = 10.0;
|
||||
snapshot.open = 11.0;
|
||||
snapshot.close = 20.0;
|
||||
let data = DataSet::from_components_with_actions_and_quotes(
|
||||
vec![limit_test_instrument()],
|
||||
vec![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(20_000.0);
|
||||
portfolio.position_mut("000001.SZ").buy(
|
||||
date.pred_opt().expect("previous date"),
|
||||
1_000,
|
||||
10.0,
|
||||
);
|
||||
portfolio.apply_cash_delta(-10_000.0);
|
||||
let mut report = BrokerExecutionReport::default();
|
||||
|
||||
broker
|
||||
.process_target_value(
|
||||
date,
|
||||
&mut portfolio,
|
||||
&data,
|
||||
"000001.SZ",
|
||||
5_500.0,
|
||||
"next_open_target_value",
|
||||
&mut BTreeMap::new(),
|
||||
&mut BTreeMap::new(),
|
||||
&mut None,
|
||||
&mut BTreeMap::new(),
|
||||
&mut report,
|
||||
)
|
||||
.expect("target value execution");
|
||||
|
||||
assert_eq!(report.fill_events.len(), 1);
|
||||
assert_eq!(report.fill_events[0].price, 11.0);
|
||||
assert_eq!(report.fill_events[0].quantity, 500);
|
||||
assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 500);
|
||||
}
|
||||
|
||||
#[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");
|
||||
@@ -7238,6 +7490,51 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_weight_buy_quantity_respects_per_symbol_budget_after_slippage_and_fees() {
|
||||
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_matching_type(MatchingType::NextBarOpen)
|
||||
.with_slippage_model(SlippageModel::PriceRatio(0.002))
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false)
|
||||
.with_inactive_limit(false);
|
||||
let mut snapshot = limit_test_snapshot();
|
||||
snapshot.date = date;
|
||||
snapshot.open = 10.0;
|
||||
snapshot.close = 10.0;
|
||||
snapshot.last_price = 10.0;
|
||||
let data = DataSet::from_components_with_actions_and_quotes(
|
||||
vec![limit_test_instrument()],
|
||||
vec![snapshot],
|
||||
Vec::new(),
|
||||
vec![limit_test_candidate(true, true)],
|
||||
vec![limit_test_benchmark()],
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
)
|
||||
.expect("valid dataset");
|
||||
let portfolio = PortfolioState::new(100_000.0);
|
||||
let target_weights = BTreeMap::from([("000001.SZ".to_string(), 0.5)]);
|
||||
|
||||
let (targets, _) = broker
|
||||
.target_quantities(date, &portfolio, &data, &target_weights)
|
||||
.expect("target quantities");
|
||||
let quantity = targets["000001.SZ"];
|
||||
let execution_price = 10.0 * 1.002;
|
||||
let allocated_amount = 50_000.0;
|
||||
|
||||
assert_eq!(quantity, 4_900);
|
||||
assert!(broker.estimated_buy_cash_out(date, execution_price, quantity) <= allocated_amount);
|
||||
assert!(
|
||||
broker.estimated_buy_cash_out(date, execution_price, quantity + 100) > allocated_amount
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_portfolio_smart_records_buy_rejection_when_target_is_blacklisted() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||
@@ -7344,7 +7641,7 @@ mod tests {
|
||||
let (aiquant_targets, _) = aiquant_broker
|
||||
.target_quantities(date, &portfolio, &data, &target_weights)
|
||||
.expect("aiquant target quantities");
|
||||
assert_eq!(aiquant_targets.get("000001.SZ").copied(), Some(50_000));
|
||||
assert_eq!(aiquant_targets.get("000001.SZ").copied(), Some(49_900));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -7558,7 +7855,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_portfolio_smart_scales_buys_when_full_targets_exceed_cash_by_fees() {
|
||||
fn target_portfolio_smart_budgets_each_buy_before_cash_optimization() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||
let symbols = ["000001.SZ", "000002.SZ"];
|
||||
let instruments = symbols
|
||||
@@ -7618,20 +7915,7 @@ mod tests {
|
||||
|
||||
assert_eq!(target_quantities.get("000001.SZ").copied(), Some(400));
|
||||
assert_eq!(target_quantities.get("000002.SZ").copied(), Some(400));
|
||||
assert!(
|
||||
diagnostics
|
||||
.iter()
|
||||
.any(|line| line.contains("rebalance_safety_scaled")),
|
||||
"{diagnostics:?}"
|
||||
);
|
||||
assert!(
|
||||
diagnostics
|
||||
.iter()
|
||||
.any(|line| line.contains("rebalance_buy_reduced")
|
||||
&& line.contains("provisional=500")
|
||||
&& line.contains("final=400")),
|
||||
"{diagnostics:?}"
|
||||
);
|
||||
assert!(diagnostics.is_empty(), "{diagnostics:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user