修正AiQuant兼容回测盘中估值口径
This commit is contained in:
@@ -361,30 +361,12 @@ where
|
||||
symbol: &str,
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
) -> f64 {
|
||||
if self.execution_price_field == PriceField::Last {
|
||||
let start_cursor = self
|
||||
.runtime_intraday_start_time
|
||||
.get()
|
||||
.or(self.intraday_execution_start_time)
|
||||
.map(|start_time| date.and_time(start_time));
|
||||
if let Some(price) = self
|
||||
.latest_known_quote_at_or_before(
|
||||
data.execution_quotes_on(date, symbol),
|
||||
start_cursor,
|
||||
snapshot,
|
||||
OrderSide::Buy,
|
||||
MatchingType::NextTickLast,
|
||||
false,
|
||||
)
|
||||
.and_then(|quote| {
|
||||
(quote.last_price.is_finite() && quote.last_price > 0.0)
|
||||
.then_some(quote.last_price)
|
||||
})
|
||||
{
|
||||
return price;
|
||||
}
|
||||
let _ = (date, data, symbol);
|
||||
if snapshot.close.is_finite() && snapshot.close > 0.0 {
|
||||
snapshot.close
|
||||
} else {
|
||||
self.sizing_price(snapshot)
|
||||
}
|
||||
self.sizing_price(snapshot)
|
||||
}
|
||||
|
||||
fn value_order_sizing_price(
|
||||
@@ -3675,16 +3657,11 @@ where
|
||||
requested_qty
|
||||
}
|
||||
|
||||
fn value_buy_gross_limit(
|
||||
&self,
|
||||
value_budget: Option<f64>,
|
||||
requested_qty: u32,
|
||||
reference_price: f64,
|
||||
) -> Option<f64> {
|
||||
fn value_buy_gross_limit(&self, value_budget: Option<f64>) -> Option<f64> {
|
||||
if !self.strict_value_budget {
|
||||
return None;
|
||||
}
|
||||
value_budget.map(|budget| budget.max(reference_price * requested_qty as f64))
|
||||
value_budget.filter(|budget| budget.is_finite() && *budget > 0.0)
|
||||
}
|
||||
|
||||
fn process_buy(
|
||||
@@ -3843,8 +3820,15 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let value_gross_limit =
|
||||
self.value_buy_gross_limit(value_budget, constrained_qty, self.sizing_price(snapshot));
|
||||
let value_gross_limit = self.value_buy_gross_limit(value_budget);
|
||||
let buy_cash_limit = if self.strict_value_budget {
|
||||
value_budget
|
||||
.filter(|budget| budget.is_finite() && *budget > 0.0)
|
||||
.map(|budget| portfolio.cash().min(budget))
|
||||
.unwrap_or_else(|| portfolio.cash())
|
||||
} else {
|
||||
portfolio.cash()
|
||||
};
|
||||
|
||||
let fill = self.resolve_execution_fill(
|
||||
date,
|
||||
@@ -3859,7 +3843,7 @@ where
|
||||
false,
|
||||
execution_cursors,
|
||||
None,
|
||||
Some(portfolio.cash()),
|
||||
Some(buy_cash_limit),
|
||||
value_gross_limit,
|
||||
algo_request,
|
||||
limit_price,
|
||||
@@ -3896,7 +3880,7 @@ where
|
||||
self.execution_price_with_limit_slippage(execution_price, limit_price);
|
||||
let filled_qty = self.affordable_buy_quantity(
|
||||
date,
|
||||
portfolio.cash(),
|
||||
buy_cash_limit,
|
||||
value_gross_limit,
|
||||
execution_price,
|
||||
constrained_qty,
|
||||
@@ -3913,7 +3897,7 @@ where
|
||||
partial_fill_reason = merge_partial_fill_reason(
|
||||
partial_fill_reason,
|
||||
self.buy_reduction_reason(
|
||||
portfolio.cash(),
|
||||
buy_cash_limit,
|
||||
value_gross_limit,
|
||||
execution_price,
|
||||
constrained_qty,
|
||||
@@ -5119,7 +5103,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_value_sizing_uses_intraday_tick_instead_of_daily_snapshot() {
|
||||
fn target_value_valuation_uses_daily_snapshot_but_value_order_sizing_uses_intraday_tick() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
@@ -5128,7 +5112,7 @@ mod tests {
|
||||
)
|
||||
.with_intraday_execution_start_time(date.and_hms_opt(9, 33, 0).unwrap().time());
|
||||
let mut snapshot = limit_test_snapshot();
|
||||
snapshot.last_price = 10.0;
|
||||
snapshot.last_price = 9.0;
|
||||
snapshot.close = 10.0;
|
||||
let mut quote = limit_test_quote(11.0, 10.99, 11.01);
|
||||
quote.timestamp = date.and_hms_opt(9, 32, 58).expect("valid timestamp");
|
||||
@@ -5146,7 +5130,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
broker.target_value_valuation_price(date, &data, "000001.SZ", snapshot),
|
||||
11.0
|
||||
10.0
|
||||
);
|
||||
assert_eq!(
|
||||
broker.value_sell_sizing_price(date, &data, "000001.SZ", snapshot),
|
||||
@@ -5266,6 +5250,69 @@ mod tests {
|
||||
assert_eq!(report.order_events[0].filled_quantity, 14_300);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_value_buy_budget_includes_commission_at_execution_price() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks,
|
||||
PriceField::Last,
|
||||
)
|
||||
.with_intraday_execution_start_time(date.and_hms_opt(9, 33, 0).unwrap().time())
|
||||
.with_slippage_model(SlippageModel::PriceRatio(0.002))
|
||||
.with_strict_value_budget(true)
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false)
|
||||
.with_inactive_limit(false);
|
||||
let mut snapshot = limit_test_snapshot();
|
||||
snapshot.day_open = 7.14;
|
||||
snapshot.open = 7.14;
|
||||
snapshot.high = 7.20;
|
||||
snapshot.low = 7.10;
|
||||
snapshot.last_price = 7.14;
|
||||
snapshot.bid1 = 7.14;
|
||||
snapshot.ask1 = 7.15;
|
||||
snapshot.close = 7.14;
|
||||
snapshot.upper_limit = 7.85;
|
||||
snapshot.lower_limit = 6.43;
|
||||
let mut quote = limit_test_quote(7.14, 7.14, 7.15);
|
||||
quote.timestamp = date.and_hms_opt(9, 32, 55).expect("valid timestamp");
|
||||
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![quote],
|
||||
)
|
||||
.expect("valid dataset");
|
||||
let value_budget = 125_216.8131;
|
||||
let mut portfolio = PortfolioState::new(10_000_000.0);
|
||||
let mut report = BrokerExecutionReport::default();
|
||||
|
||||
broker
|
||||
.process_value(
|
||||
date,
|
||||
&mut portfolio,
|
||||
&data,
|
||||
"000001.SZ",
|
||||
value_budget,
|
||||
"periodic_rebalance_buy",
|
||||
&mut BTreeMap::new(),
|
||||
&mut BTreeMap::new(),
|
||||
&mut None,
|
||||
&mut BTreeMap::new(),
|
||||
&mut report,
|
||||
)
|
||||
.expect("value buy processed");
|
||||
|
||||
let fill = report.fill_events.first().expect("fill event");
|
||||
assert_eq!(fill.quantity, 17_400);
|
||||
assert!(fill.gross_amount + fill.commission <= value_budget + 1e-6);
|
||||
assert!((fill.price - 7.15428).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instantaneous_twap_without_limits_does_not_cap_quote_quantity() {
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
|
||||
@@ -915,20 +915,30 @@ impl PlatformExprStrategy {
|
||||
if position.quantity == 0 {
|
||||
continue;
|
||||
}
|
||||
let mark_price = ctx
|
||||
.data
|
||||
.price(date, &position.symbol, PriceField::Close)
|
||||
.or_else(|| ctx.data.price(date, &position.symbol, PriceField::Last))
|
||||
.or_else(|| {
|
||||
ctx.data
|
||||
.price_on_or_before(date, &position.symbol, PriceField::Close)
|
||||
})
|
||||
.or_else(|| {
|
||||
ctx.data
|
||||
.price_on_or_before(date, &position.symbol, PriceField::Last)
|
||||
})
|
||||
.filter(|price| price.is_finite() && *price > 0.0)
|
||||
.unwrap_or(position.last_price);
|
||||
let mark_price = if self.config.aiquant_transaction_cost {
|
||||
ctx.data
|
||||
.price(date, &position.symbol, PriceField::Last)
|
||||
.or_else(|| {
|
||||
ctx.data
|
||||
.price_on_or_before(date, &position.symbol, PriceField::Last)
|
||||
})
|
||||
.filter(|price| price.is_finite() && *price > 0.0)
|
||||
.unwrap_or(position.last_price)
|
||||
} else {
|
||||
ctx.data
|
||||
.price(date, &position.symbol, PriceField::Close)
|
||||
.or_else(|| ctx.data.price(date, &position.symbol, PriceField::Last))
|
||||
.or_else(|| {
|
||||
ctx.data
|
||||
.price_on_or_before(date, &position.symbol, PriceField::Close)
|
||||
})
|
||||
.or_else(|| {
|
||||
ctx.data
|
||||
.price_on_or_before(date, &position.symbol, PriceField::Last)
|
||||
})
|
||||
.filter(|price| price.is_finite() && *price > 0.0)
|
||||
.unwrap_or(position.last_price)
|
||||
};
|
||||
if mark_price.is_finite() && mark_price > 0.0 {
|
||||
total += mark_price * position.quantity as f64;
|
||||
}
|
||||
@@ -1279,6 +1289,108 @@ impl PlatformExprStrategy {
|
||||
Some(fill.quantity)
|
||||
}
|
||||
|
||||
fn project_target_value(
|
||||
&self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
projected: &mut PortfolioState,
|
||||
date: NaiveDate,
|
||||
symbol: &str,
|
||||
target_value: f64,
|
||||
execution_state: &mut ProjectedExecutionState,
|
||||
) -> Option<u32> {
|
||||
let current_qty = projected.position(symbol)?.quantity;
|
||||
if current_qty == 0 {
|
||||
return None;
|
||||
}
|
||||
if target_value <= f64::EPSILON {
|
||||
return self.project_target_zero(ctx, projected, date, symbol, execution_state);
|
||||
}
|
||||
let market = ctx.data.market(date, symbol)?;
|
||||
let valuation_price = if market.close.is_finite() && market.close > 0.0 {
|
||||
market.close
|
||||
} else {
|
||||
self.projected_execution_price(market, OrderSide::Buy)
|
||||
};
|
||||
if !valuation_price.is_finite() || valuation_price <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let current_value = valuation_price * current_qty as f64;
|
||||
let cash_delta = target_value.max(0.0) - current_value;
|
||||
if cash_delta.abs() <= f64::EPSILON {
|
||||
return None;
|
||||
}
|
||||
if cash_delta > 0.0 {
|
||||
return Some(self.project_order_value(
|
||||
ctx,
|
||||
projected,
|
||||
date,
|
||||
symbol,
|
||||
cash_delta,
|
||||
execution_state,
|
||||
));
|
||||
}
|
||||
|
||||
if !self.can_sell_position(ctx, date, symbol) {
|
||||
return None;
|
||||
}
|
||||
let sizing_price = self
|
||||
.aiquant_scheduled_quote(ctx, date, symbol)
|
||||
.and_then(|quote| {
|
||||
if quote.last_price.is_finite() && quote.last_price > 0.0 {
|
||||
Some(quote.last_price)
|
||||
} else {
|
||||
quote.sell_price()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| self.projected_execution_price(market, OrderSide::Sell));
|
||||
if !sizing_price.is_finite() || sizing_price <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let round_lot = self.projected_round_lot(ctx, symbol);
|
||||
let minimum_order_quantity = self.projected_minimum_order_quantity(ctx, symbol);
|
||||
let order_step_size = self.projected_order_step_size(ctx, symbol);
|
||||
let requested_qty = self
|
||||
.round_lot_quantity(
|
||||
((cash_delta.abs()) / sizing_price).floor() as u32,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
)
|
||||
.min(current_qty);
|
||||
if requested_qty == 0 {
|
||||
return None;
|
||||
}
|
||||
let fill = self.projected_select_execution_fill(
|
||||
ctx,
|
||||
date,
|
||||
symbol,
|
||||
OrderSide::Sell,
|
||||
requested_qty,
|
||||
round_lot,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
requested_qty >= current_qty,
|
||||
None,
|
||||
None,
|
||||
execution_state,
|
||||
)?;
|
||||
let gross_amount = fill.price * fill.quantity as f64;
|
||||
let sell_cost = self.sell_cost(date, gross_amount);
|
||||
projected
|
||||
.position_mut(symbol)
|
||||
.sell(fill.quantity, fill.price)
|
||||
.ok()?;
|
||||
projected.apply_cash_delta(gross_amount - sell_cost);
|
||||
*execution_state
|
||||
.intraday_turnover
|
||||
.entry(symbol.to_string())
|
||||
.or_default() += fill.quantity;
|
||||
execution_state
|
||||
.execution_cursors
|
||||
.insert(symbol.to_string(), fill.next_cursor);
|
||||
projected.prune_flat_positions();
|
||||
Some(fill.quantity)
|
||||
}
|
||||
|
||||
fn projected_position_count_excluding(
|
||||
projected: &PortfolioState,
|
||||
excluded_symbols: &BTreeSet<String>,
|
||||
@@ -1317,6 +1429,17 @@ impl PlatformExprStrategy {
|
||||
let Some(market) = ctx.data.market(date, symbol) else {
|
||||
return position.market_value();
|
||||
};
|
||||
if self.config.aiquant_transaction_cost {
|
||||
if let Some(price) = self
|
||||
.aiquant_scheduled_quote(ctx, date, symbol)
|
||||
.and_then(|quote| {
|
||||
(quote.last_price.is_finite() && quote.last_price > 0.0)
|
||||
.then_some(quote.last_price)
|
||||
})
|
||||
{
|
||||
return position.quantity as f64 * price;
|
||||
}
|
||||
}
|
||||
let execution_price = self.projected_execution_price(market, OrderSide::Buy);
|
||||
if execution_price.is_finite() && execution_price > 0.0 {
|
||||
position.quantity as f64 * execution_price
|
||||
@@ -1325,24 +1448,6 @@ impl PlatformExprStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
fn projected_position_value_excluding(
|
||||
&self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
projected: &PortfolioState,
|
||||
date: NaiveDate,
|
||||
excluded_symbols: &BTreeSet<String>,
|
||||
) -> f64 {
|
||||
projected
|
||||
.positions()
|
||||
.keys()
|
||||
.filter(|symbol| !excluded_symbols.contains(*symbol))
|
||||
.map(|symbol| {
|
||||
self.projected_position_value_at_execution_price(ctx, projected, date, symbol)
|
||||
})
|
||||
.filter(|value| value.is_finite() && *value > 0.0)
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn remaining_buy_cash_per_slot(
|
||||
&self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
@@ -1350,20 +1455,27 @@ impl PlatformExprStrategy {
|
||||
date: NaiveDate,
|
||||
target_budget: f64,
|
||||
selection_limit: usize,
|
||||
excluded_symbols: &BTreeSet<String>,
|
||||
working_symbols: &BTreeSet<String>,
|
||||
value_symbols: &BTreeSet<String>,
|
||||
pending_buy_value: f64,
|
||||
) -> f64 {
|
||||
if selection_limit == 0 || !target_budget.is_finite() || target_budget <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let active_count = Self::projected_position_count_excluding(projected, excluded_symbols);
|
||||
if active_count >= selection_limit {
|
||||
if working_symbols.len() >= selection_limit {
|
||||
return 0.0;
|
||||
}
|
||||
let slots_remaining = selection_limit.saturating_sub(active_count).max(1);
|
||||
let active_value =
|
||||
self.projected_position_value_excluding(ctx, projected, date, excluded_symbols);
|
||||
let remaining_budget = (target_budget - active_value).max(0.0);
|
||||
remaining_budget / slots_remaining as f64
|
||||
let slots_remaining = selection_limit.saturating_sub(working_symbols.len()).max(1);
|
||||
let active_value = working_symbols
|
||||
.iter()
|
||||
.filter(|symbol| value_symbols.contains(*symbol))
|
||||
.map(|symbol| {
|
||||
self.projected_position_value_at_execution_price(ctx, projected, date, symbol)
|
||||
})
|
||||
.filter(|value| value.is_finite() && *value > 0.0)
|
||||
.sum::<f64>();
|
||||
let working_value = active_value + pending_buy_value.max(0.0);
|
||||
(target_budget - working_value).max(0.0) / slots_remaining as f64
|
||||
}
|
||||
|
||||
fn project_order_value(
|
||||
@@ -1413,14 +1525,19 @@ impl PlatformExprStrategy {
|
||||
);
|
||||
let mut quantity = snapshot_requested_qty;
|
||||
let gross_limit = if self.config.strict_value_budget {
|
||||
Some(order_value.max(execution_price * quantity as f64))
|
||||
Some(order_value)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let cash_limit = if self.config.strict_value_budget {
|
||||
projected.cash().min(order_value)
|
||||
} else {
|
||||
projected.cash()
|
||||
};
|
||||
while quantity > 0 {
|
||||
let gross_amount = execution_price * quantity as f64;
|
||||
if gross_limit.map_or(true, |limit| gross_amount <= limit + 1e-6)
|
||||
&& gross_amount + self.buy_commission(gross_amount) <= projected.cash() + 1e-6
|
||||
&& gross_amount + self.buy_commission(gross_amount) <= cash_limit + 1e-6
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -1441,7 +1558,7 @@ impl PlatformExprStrategy {
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
false,
|
||||
Some(projected.cash()),
|
||||
Some(cash_limit),
|
||||
gross_limit,
|
||||
execution_state,
|
||||
)
|
||||
@@ -1462,7 +1579,7 @@ impl PlatformExprStrategy {
|
||||
};
|
||||
let gross_amount = fill.price * fill.quantity as f64;
|
||||
let cash_out = gross_amount + self.buy_commission(gross_amount);
|
||||
if cash_out > projected.cash() + 1e-6 {
|
||||
if cash_out > cash_limit + 1e-6 {
|
||||
return 0;
|
||||
}
|
||||
projected.apply_cash_delta(-cash_out);
|
||||
@@ -5926,7 +6043,16 @@ impl Strategy for PlatformExprStrategy {
|
||||
target_value,
|
||||
reason: "daily_position_target_adjust".to_string(),
|
||||
});
|
||||
self.project_target_value(
|
||||
ctx,
|
||||
&mut projected,
|
||||
execution_date,
|
||||
&position.symbol,
|
||||
target_value,
|
||||
&mut projected_execution_state,
|
||||
);
|
||||
}
|
||||
aiquant_available_cash = projected.cash();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5992,85 +6118,109 @@ impl Strategy for PlatformExprStrategy {
|
||||
.insert(position.symbol.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.config.daily_top_up_enabled
|
||||
&& self.config.rotation_enabled
|
||||
&& trading_ratio > 0.0
|
||||
&& {
|
||||
let excluded_symbols = Self::pending_exit_exclusion_symbols(
|
||||
&unresolved_stop_loss_symbols,
|
||||
&exit_symbols,
|
||||
&delayed_sold_symbols,
|
||||
);
|
||||
Self::projected_position_count_excluding(&projected, &excluded_symbols)
|
||||
< selection_limit
|
||||
}
|
||||
{
|
||||
let target_budget = aiquant_total_value * trading_ratio;
|
||||
if self.config.daily_top_up_enabled
|
||||
&& self.config.rotation_enabled
|
||||
&& trading_ratio > 0.0
|
||||
&& selection_limit > 0
|
||||
{
|
||||
let target_budget = aiquant_total_value * trading_ratio;
|
||||
let mut working_symbols = projected
|
||||
.positions()
|
||||
.keys()
|
||||
.filter(|symbol| {
|
||||
!same_day_sold_symbols.contains(*symbol)
|
||||
&& !exit_symbols.contains(*symbol)
|
||||
&& !delayed_sold_symbols.contains(*symbol)
|
||||
&& !unresolved_stop_loss_symbols.contains(*symbol)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
let value_symbols = working_symbols.clone();
|
||||
let mut pending_buy_value = 0.0_f64;
|
||||
loop {
|
||||
let excluded_symbols = Self::pending_exit_exclusion_symbols(
|
||||
&unresolved_stop_loss_symbols,
|
||||
&exit_symbols,
|
||||
&delayed_sold_symbols,
|
||||
);
|
||||
if Self::projected_position_count_excluding(&projected, &excluded_symbols)
|
||||
>= selection_limit
|
||||
{
|
||||
break;
|
||||
}
|
||||
let slot_buy_cash = self.remaining_buy_cash_per_slot(
|
||||
ctx,
|
||||
&projected,
|
||||
execution_date,
|
||||
target_budget,
|
||||
selection_limit,
|
||||
&excluded_symbols,
|
||||
&working_symbols,
|
||||
&value_symbols,
|
||||
pending_buy_value,
|
||||
);
|
||||
let available_buy_cash = slot_buy_cash.min(aiquant_available_cash);
|
||||
if slot_buy_cash > 0.0 && available_buy_cash >= slot_buy_cash * 0.5 {
|
||||
for symbol in &stock_list {
|
||||
if symbol == &position.symbol
|
||||
|| projected.positions().contains_key(symbol)
|
||||
|| same_day_sold_symbols.contains(symbol)
|
||||
|| exit_symbols.contains(symbol)
|
||||
|| delayed_sold_symbols.contains(symbol)
|
||||
|| intraday_attempted_buys.contains(symbol)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let execution_stock = self.stock_state(ctx, execution_date, symbol)?;
|
||||
if self
|
||||
.buy_rejection_reason(ctx, execution_date, symbol, &execution_stock)?
|
||||
.is_some()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let decision_stock = self.stock_state_with_factor_date(
|
||||
ctx,
|
||||
decision_date,
|
||||
selection_factor_date,
|
||||
symbol,
|
||||
)?;
|
||||
let buy_cash =
|
||||
available_buy_cash * self.buy_scale(ctx, &day, &decision_stock)?;
|
||||
if buy_cash <= 0.0 {
|
||||
break;
|
||||
}
|
||||
order_intents.push(OrderIntent::Value {
|
||||
symbol: symbol.clone(),
|
||||
value: buy_cash,
|
||||
reason: "daily_top_up_buy".to_string(),
|
||||
});
|
||||
let cash_before_buy = projected.cash();
|
||||
let filled_qty = self.project_order_value(
|
||||
ctx,
|
||||
&mut projected,
|
||||
execution_date,
|
||||
symbol,
|
||||
buy_cash,
|
||||
&mut projected_execution_state,
|
||||
);
|
||||
if filled_qty > 0 {
|
||||
let spent = (cash_before_buy - projected.cash()).max(0.0);
|
||||
aiquant_available_cash = (aiquant_available_cash - spent).max(0.0);
|
||||
intraday_attempted_buys.insert(symbol.clone());
|
||||
}
|
||||
if slot_buy_cash <= 0.0 || available_buy_cash < slot_buy_cash * 0.5 {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut attempted_any = false;
|
||||
let mut filled_any = false;
|
||||
for symbol in &stock_list {
|
||||
if projected.positions().contains_key(symbol)
|
||||
|| same_day_sold_symbols.contains(symbol)
|
||||
|| exit_symbols.contains(symbol)
|
||||
|| delayed_sold_symbols.contains(symbol)
|
||||
|| intraday_attempted_buys.contains(symbol)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let execution_stock = self.stock_state(ctx, execution_date, symbol)?;
|
||||
if self
|
||||
.buy_rejection_reason(ctx, execution_date, symbol, &execution_stock)?
|
||||
.is_some()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let decision_stock = self.stock_state_with_factor_date(
|
||||
ctx,
|
||||
decision_date,
|
||||
selection_factor_date,
|
||||
symbol,
|
||||
)?;
|
||||
let buy_cash =
|
||||
available_buy_cash * self.buy_scale(ctx, &day, &decision_stock)?;
|
||||
if buy_cash <= 0.0 {
|
||||
break;
|
||||
}
|
||||
order_intents.push(OrderIntent::Value {
|
||||
symbol: symbol.clone(),
|
||||
value: buy_cash,
|
||||
reason: "daily_top_up_buy".to_string(),
|
||||
});
|
||||
intraday_attempted_buys.insert(symbol.clone());
|
||||
attempted_any = true;
|
||||
let cash_before_buy = projected.cash();
|
||||
let filled_qty = self.project_order_value(
|
||||
ctx,
|
||||
&mut projected,
|
||||
execution_date,
|
||||
symbol,
|
||||
buy_cash,
|
||||
&mut projected_execution_state,
|
||||
);
|
||||
if filled_qty > 0 {
|
||||
let spent = (cash_before_buy - projected.cash()).max(0.0);
|
||||
aiquant_available_cash = (aiquant_available_cash - spent).max(0.0);
|
||||
working_symbols.insert(symbol.clone());
|
||||
pending_buy_value += available_buy_cash;
|
||||
filled_any = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !attempted_any || !filled_any {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6112,6 +6262,19 @@ impl Strategy for PlatformExprStrategy {
|
||||
}
|
||||
let fixed_buy_cash = aiquant_total_value * trading_ratio / selection_limit as f64;
|
||||
let target_budget = aiquant_total_value * trading_ratio;
|
||||
let mut rebalance_working_symbols = projected
|
||||
.positions()
|
||||
.keys()
|
||||
.filter(|symbol| {
|
||||
!same_day_sold_symbols.contains(*symbol)
|
||||
&& !exit_symbols.contains(*symbol)
|
||||
&& !delayed_sold_symbols.contains(*symbol)
|
||||
&& !unresolved_stop_loss_symbols.contains(*symbol)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
let rebalance_value_symbols = rebalance_working_symbols.clone();
|
||||
let mut rebalance_pending_buy_value = 0.0_f64;
|
||||
for symbol in stock_list.iter().take(selection_limit) {
|
||||
let decision_stock = self.stock_state_with_factor_date(
|
||||
ctx,
|
||||
@@ -6191,11 +6354,9 @@ impl Strategy for PlatformExprStrategy {
|
||||
execution_date,
|
||||
target_budget,
|
||||
selection_limit,
|
||||
&Self::pending_exit_exclusion_symbols(
|
||||
&unresolved_stop_loss_symbols,
|
||||
&exit_symbols,
|
||||
&delayed_sold_symbols,
|
||||
),
|
||||
&rebalance_working_symbols,
|
||||
&rebalance_value_symbols,
|
||||
rebalance_pending_buy_value,
|
||||
);
|
||||
let target_cash = slot_buy_cash * stock_scale;
|
||||
let buy_cash = target_cash.min(aiquant_available_cash);
|
||||
@@ -6232,6 +6393,8 @@ impl Strategy for PlatformExprStrategy {
|
||||
if filled_qty > 0 {
|
||||
let spent = (cash_before_buy - projected.cash()).max(0.0);
|
||||
aiquant_available_cash = (aiquant_available_cash - spent).max(0.0);
|
||||
rebalance_working_symbols.insert(symbol.clone());
|
||||
rebalance_pending_buy_value += buy_cash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7986,6 +8149,102 @@ mod tests {
|
||||
assert_eq!(quote.last_price, 19.8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn platform_aiquant_marked_total_uses_intraday_last_not_close() {
|
||||
let date = d(2025, 1, 6);
|
||||
let symbol = "603779.SH";
|
||||
let data = DataSet::from_components_with_actions_and_quotes(
|
||||
vec![Instrument {
|
||||
symbol: symbol.to_string(),
|
||||
name: symbol.to_string(),
|
||||
board: "SH".to_string(),
|
||||
round_lot: 100,
|
||||
listed_at: Some(d(2020, 1, 1)),
|
||||
delisted_at: None,
|
||||
status: "active".to_string(),
|
||||
}],
|
||||
vec![DailyMarketSnapshot {
|
||||
date,
|
||||
symbol: symbol.to_string(),
|
||||
timestamp: Some("2025-01-06 10:40:00".to_string()),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
high: 22.0,
|
||||
low: 9.8,
|
||||
close: 21.5,
|
||||
last_price: 10.0,
|
||||
bid1: 9.99,
|
||||
ask1: 10.0,
|
||||
prev_close: 10.0,
|
||||
volume: 1_000_000,
|
||||
tick_volume: 1_000,
|
||||
bid1_volume: 1_000,
|
||||
ask1_volume: 1_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 22.0,
|
||||
lower_limit: 8.0,
|
||||
price_tick: 0.01,
|
||||
}],
|
||||
vec![DailyFactorSnapshot {
|
||||
date,
|
||||
symbol: symbol.to_string(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 8.0,
|
||||
pe_ttm: 8.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
extra_factors: BTreeMap::new(),
|
||||
}],
|
||||
vec![CandidateEligibility {
|
||||
date,
|
||||
symbol: symbol.to_string(),
|
||||
is_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
}],
|
||||
vec![BenchmarkSnapshot {
|
||||
date,
|
||||
benchmark: "000852.SH".to_string(),
|
||||
open: 1000.0,
|
||||
close: 1002.0,
|
||||
prev_close: 998.0,
|
||||
volume: 1_000_000,
|
||||
}],
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
)
|
||||
.expect("dataset");
|
||||
let subscriptions = BTreeSet::new();
|
||||
let mut portfolio = PortfolioState::new(1_000.0);
|
||||
portfolio.position_mut(symbol).buy(date, 100, 9.0);
|
||||
let ctx = StrategyContext {
|
||||
execution_date: date,
|
||||
decision_date: date,
|
||||
decision_index: 1,
|
||||
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.aiquant_transaction_cost = true;
|
||||
let strategy = PlatformExprStrategy::new(cfg);
|
||||
assert_eq!(strategy.marked_total_value(&ctx, date), 2_000.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn platform_aiquant_intraday_selection_filters_limits_on_execution_date() {
|
||||
let factor_date = d(2023, 11, 10);
|
||||
|
||||
Reference in New Issue
Block a user