修正AiQuant兼容回测盘中估值口径
This commit is contained in:
@@ -361,30 +361,12 @@ where
|
|||||||
symbol: &str,
|
symbol: &str,
|
||||||
snapshot: &crate::data::DailyMarketSnapshot,
|
snapshot: &crate::data::DailyMarketSnapshot,
|
||||||
) -> f64 {
|
) -> f64 {
|
||||||
if self.execution_price_field == PriceField::Last {
|
let _ = (date, data, symbol);
|
||||||
let start_cursor = self
|
if snapshot.close.is_finite() && snapshot.close > 0.0 {
|
||||||
.runtime_intraday_start_time
|
snapshot.close
|
||||||
.get()
|
} else {
|
||||||
.or(self.intraday_execution_start_time)
|
self.sizing_price(snapshot)
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.sizing_price(snapshot)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn value_order_sizing_price(
|
fn value_order_sizing_price(
|
||||||
@@ -3675,16 +3657,11 @@ where
|
|||||||
requested_qty
|
requested_qty
|
||||||
}
|
}
|
||||||
|
|
||||||
fn value_buy_gross_limit(
|
fn value_buy_gross_limit(&self, value_budget: Option<f64>) -> Option<f64> {
|
||||||
&self,
|
|
||||||
value_budget: Option<f64>,
|
|
||||||
requested_qty: u32,
|
|
||||||
reference_price: f64,
|
|
||||||
) -> Option<f64> {
|
|
||||||
if !self.strict_value_budget {
|
if !self.strict_value_budget {
|
||||||
return None;
|
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(
|
fn process_buy(
|
||||||
@@ -3843,8 +3820,15 @@ where
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let value_gross_limit =
|
let value_gross_limit = self.value_buy_gross_limit(value_budget);
|
||||||
self.value_buy_gross_limit(value_budget, constrained_qty, self.sizing_price(snapshot));
|
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(
|
let fill = self.resolve_execution_fill(
|
||||||
date,
|
date,
|
||||||
@@ -3859,7 +3843,7 @@ where
|
|||||||
false,
|
false,
|
||||||
execution_cursors,
|
execution_cursors,
|
||||||
None,
|
None,
|
||||||
Some(portfolio.cash()),
|
Some(buy_cash_limit),
|
||||||
value_gross_limit,
|
value_gross_limit,
|
||||||
algo_request,
|
algo_request,
|
||||||
limit_price,
|
limit_price,
|
||||||
@@ -3896,7 +3880,7 @@ where
|
|||||||
self.execution_price_with_limit_slippage(execution_price, limit_price);
|
self.execution_price_with_limit_slippage(execution_price, limit_price);
|
||||||
let filled_qty = self.affordable_buy_quantity(
|
let filled_qty = self.affordable_buy_quantity(
|
||||||
date,
|
date,
|
||||||
portfolio.cash(),
|
buy_cash_limit,
|
||||||
value_gross_limit,
|
value_gross_limit,
|
||||||
execution_price,
|
execution_price,
|
||||||
constrained_qty,
|
constrained_qty,
|
||||||
@@ -3913,7 +3897,7 @@ where
|
|||||||
partial_fill_reason = merge_partial_fill_reason(
|
partial_fill_reason = merge_partial_fill_reason(
|
||||||
partial_fill_reason,
|
partial_fill_reason,
|
||||||
self.buy_reduction_reason(
|
self.buy_reduction_reason(
|
||||||
portfolio.cash(),
|
buy_cash_limit,
|
||||||
value_gross_limit,
|
value_gross_limit,
|
||||||
execution_price,
|
execution_price,
|
||||||
constrained_qty,
|
constrained_qty,
|
||||||
@@ -5119,7 +5103,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||||
let broker = BrokerSimulator::new_with_execution_price(
|
let broker = BrokerSimulator::new_with_execution_price(
|
||||||
ChinaAShareCostModel::default(),
|
ChinaAShareCostModel::default(),
|
||||||
@@ -5128,7 +5112,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.with_intraday_execution_start_time(date.and_hms_opt(9, 33, 0).unwrap().time());
|
.with_intraday_execution_start_time(date.and_hms_opt(9, 33, 0).unwrap().time());
|
||||||
let mut snapshot = limit_test_snapshot();
|
let mut snapshot = limit_test_snapshot();
|
||||||
snapshot.last_price = 10.0;
|
snapshot.last_price = 9.0;
|
||||||
snapshot.close = 10.0;
|
snapshot.close = 10.0;
|
||||||
let mut quote = limit_test_quote(11.0, 10.99, 11.01);
|
let mut quote = limit_test_quote(11.0, 10.99, 11.01);
|
||||||
quote.timestamp = date.and_hms_opt(9, 32, 58).expect("valid timestamp");
|
quote.timestamp = date.and_hms_opt(9, 32, 58).expect("valid timestamp");
|
||||||
@@ -5146,7 +5130,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
broker.target_value_valuation_price(date, &data, "000001.SZ", snapshot),
|
broker.target_value_valuation_price(date, &data, "000001.SZ", snapshot),
|
||||||
11.0
|
10.0
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
broker.value_sell_sizing_price(date, &data, "000001.SZ", snapshot),
|
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);
|
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]
|
#[test]
|
||||||
fn instantaneous_twap_without_limits_does_not_cap_quote_quantity() {
|
fn instantaneous_twap_without_limits_does_not_cap_quote_quantity() {
|
||||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||||
|
|||||||
@@ -915,20 +915,30 @@ impl PlatformExprStrategy {
|
|||||||
if position.quantity == 0 {
|
if position.quantity == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mark_price = ctx
|
let mark_price = if self.config.aiquant_transaction_cost {
|
||||||
.data
|
ctx.data
|
||||||
.price(date, &position.symbol, PriceField::Close)
|
.price(date, &position.symbol, PriceField::Last)
|
||||||
.or_else(|| ctx.data.price(date, &position.symbol, PriceField::Last))
|
.or_else(|| {
|
||||||
.or_else(|| {
|
ctx.data
|
||||||
ctx.data
|
.price_on_or_before(date, &position.symbol, PriceField::Last)
|
||||||
.price_on_or_before(date, &position.symbol, PriceField::Close)
|
})
|
||||||
})
|
.filter(|price| price.is_finite() && *price > 0.0)
|
||||||
.or_else(|| {
|
.unwrap_or(position.last_price)
|
||||||
ctx.data
|
} else {
|
||||||
.price_on_or_before(date, &position.symbol, PriceField::Last)
|
ctx.data
|
||||||
})
|
.price(date, &position.symbol, PriceField::Close)
|
||||||
.filter(|price| price.is_finite() && *price > 0.0)
|
.or_else(|| ctx.data.price(date, &position.symbol, PriceField::Last))
|
||||||
.unwrap_or(position.last_price);
|
.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 {
|
if mark_price.is_finite() && mark_price > 0.0 {
|
||||||
total += mark_price * position.quantity as f64;
|
total += mark_price * position.quantity as f64;
|
||||||
}
|
}
|
||||||
@@ -1279,6 +1289,108 @@ impl PlatformExprStrategy {
|
|||||||
Some(fill.quantity)
|
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(
|
fn projected_position_count_excluding(
|
||||||
projected: &PortfolioState,
|
projected: &PortfolioState,
|
||||||
excluded_symbols: &BTreeSet<String>,
|
excluded_symbols: &BTreeSet<String>,
|
||||||
@@ -1317,6 +1429,17 @@ impl PlatformExprStrategy {
|
|||||||
let Some(market) = ctx.data.market(date, symbol) else {
|
let Some(market) = ctx.data.market(date, symbol) else {
|
||||||
return position.market_value();
|
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);
|
let execution_price = self.projected_execution_price(market, OrderSide::Buy);
|
||||||
if execution_price.is_finite() && execution_price > 0.0 {
|
if execution_price.is_finite() && execution_price > 0.0 {
|
||||||
position.quantity as f64 * execution_price
|
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(
|
fn remaining_buy_cash_per_slot(
|
||||||
&self,
|
&self,
|
||||||
ctx: &StrategyContext<'_>,
|
ctx: &StrategyContext<'_>,
|
||||||
@@ -1350,20 +1455,27 @@ impl PlatformExprStrategy {
|
|||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
target_budget: f64,
|
target_budget: f64,
|
||||||
selection_limit: usize,
|
selection_limit: usize,
|
||||||
excluded_symbols: &BTreeSet<String>,
|
working_symbols: &BTreeSet<String>,
|
||||||
|
value_symbols: &BTreeSet<String>,
|
||||||
|
pending_buy_value: f64,
|
||||||
) -> f64 {
|
) -> f64 {
|
||||||
if selection_limit == 0 || !target_budget.is_finite() || target_budget <= 0.0 {
|
if selection_limit == 0 || !target_budget.is_finite() || target_budget <= 0.0 {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
let active_count = Self::projected_position_count_excluding(projected, excluded_symbols);
|
if working_symbols.len() >= selection_limit {
|
||||||
if active_count >= selection_limit {
|
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
let slots_remaining = selection_limit.saturating_sub(active_count).max(1);
|
let slots_remaining = selection_limit.saturating_sub(working_symbols.len()).max(1);
|
||||||
let active_value =
|
let active_value = working_symbols
|
||||||
self.projected_position_value_excluding(ctx, projected, date, excluded_symbols);
|
.iter()
|
||||||
let remaining_budget = (target_budget - active_value).max(0.0);
|
.filter(|symbol| value_symbols.contains(*symbol))
|
||||||
remaining_budget / slots_remaining as f64
|
.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(
|
fn project_order_value(
|
||||||
@@ -1413,14 +1525,19 @@ impl PlatformExprStrategy {
|
|||||||
);
|
);
|
||||||
let mut quantity = snapshot_requested_qty;
|
let mut quantity = snapshot_requested_qty;
|
||||||
let gross_limit = if self.config.strict_value_budget {
|
let gross_limit = if self.config.strict_value_budget {
|
||||||
Some(order_value.max(execution_price * quantity as f64))
|
Some(order_value)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
let cash_limit = if self.config.strict_value_budget {
|
||||||
|
projected.cash().min(order_value)
|
||||||
|
} else {
|
||||||
|
projected.cash()
|
||||||
|
};
|
||||||
while quantity > 0 {
|
while quantity > 0 {
|
||||||
let gross_amount = execution_price * quantity as f64;
|
let gross_amount = execution_price * quantity as f64;
|
||||||
if gross_limit.map_or(true, |limit| gross_amount <= limit + 1e-6)
|
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;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1441,7 +1558,7 @@ impl PlatformExprStrategy {
|
|||||||
minimum_order_quantity,
|
minimum_order_quantity,
|
||||||
order_step_size,
|
order_step_size,
|
||||||
false,
|
false,
|
||||||
Some(projected.cash()),
|
Some(cash_limit),
|
||||||
gross_limit,
|
gross_limit,
|
||||||
execution_state,
|
execution_state,
|
||||||
)
|
)
|
||||||
@@ -1462,7 +1579,7 @@ impl PlatformExprStrategy {
|
|||||||
};
|
};
|
||||||
let gross_amount = fill.price * fill.quantity as f64;
|
let gross_amount = fill.price * fill.quantity as f64;
|
||||||
let cash_out = gross_amount + self.buy_commission(gross_amount);
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
projected.apply_cash_delta(-cash_out);
|
projected.apply_cash_delta(-cash_out);
|
||||||
@@ -5926,7 +6043,16 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
target_value,
|
target_value,
|
||||||
reason: "daily_position_target_adjust".to_string(),
|
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());
|
.insert(position.symbol.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if self.config.daily_top_up_enabled
|
if self.config.daily_top_up_enabled
|
||||||
&& self.config.rotation_enabled
|
&& self.config.rotation_enabled
|
||||||
&& trading_ratio > 0.0
|
&& trading_ratio > 0.0
|
||||||
&& {
|
&& selection_limit > 0
|
||||||
let excluded_symbols = Self::pending_exit_exclusion_symbols(
|
{
|
||||||
&unresolved_stop_loss_symbols,
|
let target_budget = aiquant_total_value * trading_ratio;
|
||||||
&exit_symbols,
|
let mut working_symbols = projected
|
||||||
&delayed_sold_symbols,
|
.positions()
|
||||||
);
|
.keys()
|
||||||
Self::projected_position_count_excluding(&projected, &excluded_symbols)
|
.filter(|symbol| {
|
||||||
< selection_limit
|
!same_day_sold_symbols.contains(*symbol)
|
||||||
}
|
&& !exit_symbols.contains(*symbol)
|
||||||
{
|
&& !delayed_sold_symbols.contains(*symbol)
|
||||||
let target_budget = aiquant_total_value * trading_ratio;
|
&& !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(
|
let excluded_symbols = Self::pending_exit_exclusion_symbols(
|
||||||
&unresolved_stop_loss_symbols,
|
&unresolved_stop_loss_symbols,
|
||||||
&exit_symbols,
|
&exit_symbols,
|
||||||
&delayed_sold_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(
|
let slot_buy_cash = self.remaining_buy_cash_per_slot(
|
||||||
ctx,
|
ctx,
|
||||||
&projected,
|
&projected,
|
||||||
execution_date,
|
execution_date,
|
||||||
target_budget,
|
target_budget,
|
||||||
selection_limit,
|
selection_limit,
|
||||||
&excluded_symbols,
|
&working_symbols,
|
||||||
|
&value_symbols,
|
||||||
|
pending_buy_value,
|
||||||
);
|
);
|
||||||
let available_buy_cash = slot_buy_cash.min(aiquant_available_cash);
|
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 {
|
if slot_buy_cash <= 0.0 || available_buy_cash < slot_buy_cash * 0.5 {
|
||||||
for symbol in &stock_list {
|
break;
|
||||||
if symbol == &position.symbol
|
}
|
||||||
|| projected.positions().contains_key(symbol)
|
|
||||||
|| same_day_sold_symbols.contains(symbol)
|
let mut attempted_any = false;
|
||||||
|| exit_symbols.contains(symbol)
|
let mut filled_any = false;
|
||||||
|| delayed_sold_symbols.contains(symbol)
|
for symbol in &stock_list {
|
||||||
|| intraday_attempted_buys.contains(symbol)
|
if projected.positions().contains_key(symbol)
|
||||||
{
|
|| same_day_sold_symbols.contains(symbol)
|
||||||
continue;
|
|| exit_symbols.contains(symbol)
|
||||||
}
|
|| delayed_sold_symbols.contains(symbol)
|
||||||
let execution_stock = self.stock_state(ctx, execution_date, symbol)?;
|
|| intraday_attempted_buys.contains(symbol)
|
||||||
if self
|
{
|
||||||
.buy_rejection_reason(ctx, execution_date, symbol, &execution_stock)?
|
continue;
|
||||||
.is_some()
|
}
|
||||||
{
|
let execution_stock = self.stock_state(ctx, execution_date, symbol)?;
|
||||||
continue;
|
if self
|
||||||
}
|
.buy_rejection_reason(ctx, execution_date, symbol, &execution_stock)?
|
||||||
let decision_stock = self.stock_state_with_factor_date(
|
.is_some()
|
||||||
ctx,
|
{
|
||||||
decision_date,
|
continue;
|
||||||
selection_factor_date,
|
}
|
||||||
symbol,
|
let decision_stock = self.stock_state_with_factor_date(
|
||||||
)?;
|
ctx,
|
||||||
let buy_cash =
|
decision_date,
|
||||||
available_buy_cash * self.buy_scale(ctx, &day, &decision_stock)?;
|
selection_factor_date,
|
||||||
if buy_cash <= 0.0 {
|
symbol,
|
||||||
break;
|
)?;
|
||||||
}
|
let buy_cash =
|
||||||
order_intents.push(OrderIntent::Value {
|
available_buy_cash * self.buy_scale(ctx, &day, &decision_stock)?;
|
||||||
symbol: symbol.clone(),
|
if buy_cash <= 0.0 {
|
||||||
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());
|
|
||||||
}
|
|
||||||
break;
|
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 fixed_buy_cash = aiquant_total_value * trading_ratio / selection_limit as f64;
|
||||||
let target_budget = aiquant_total_value * trading_ratio;
|
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) {
|
for symbol in stock_list.iter().take(selection_limit) {
|
||||||
let decision_stock = self.stock_state_with_factor_date(
|
let decision_stock = self.stock_state_with_factor_date(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -6191,11 +6354,9 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
execution_date,
|
execution_date,
|
||||||
target_budget,
|
target_budget,
|
||||||
selection_limit,
|
selection_limit,
|
||||||
&Self::pending_exit_exclusion_symbols(
|
&rebalance_working_symbols,
|
||||||
&unresolved_stop_loss_symbols,
|
&rebalance_value_symbols,
|
||||||
&exit_symbols,
|
rebalance_pending_buy_value,
|
||||||
&delayed_sold_symbols,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
let target_cash = slot_buy_cash * stock_scale;
|
let target_cash = slot_buy_cash * stock_scale;
|
||||||
let buy_cash = target_cash.min(aiquant_available_cash);
|
let buy_cash = target_cash.min(aiquant_available_cash);
|
||||||
@@ -6232,6 +6393,8 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
if filled_qty > 0 {
|
if filled_qty > 0 {
|
||||||
let spent = (cash_before_buy - projected.cash()).max(0.0);
|
let spent = (cash_before_buy - projected.cash()).max(0.0);
|
||||||
aiquant_available_cash = (aiquant_available_cash - spent).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);
|
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]
|
#[test]
|
||||||
fn platform_aiquant_intraday_selection_filters_limits_on_execution_date() {
|
fn platform_aiquant_intraday_selection_filters_limits_on_execution_date() {
|
||||||
let factor_date = d(2023, 11, 10);
|
let factor_date = d(2023, 11, 10);
|
||||||
|
|||||||
Reference in New Issue
Block a user