修复策略投影成交量约束
This commit is contained in:
@@ -1519,6 +1519,89 @@ impl PlatformExprStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn projected_market_fillable_quantity(
|
||||
&self,
|
||||
market: &DailyMarketSnapshot,
|
||||
quote: Option<&crate::data::IntradayExecutionQuote>,
|
||||
symbol: &str,
|
||||
side: OrderSide,
|
||||
requested_qty: u32,
|
||||
round_lot: u32,
|
||||
minimum_order_quantity: u32,
|
||||
order_step_size: u32,
|
||||
allow_odd_lot_sell: bool,
|
||||
current_fill_quantity: u32,
|
||||
execution_state: &ProjectedExecutionState,
|
||||
) -> Option<u32> {
|
||||
if requested_qty == 0 {
|
||||
return Some(0);
|
||||
}
|
||||
|
||||
let constraints = self.config.risk_config.trading_constraints;
|
||||
let mut max_fill = requested_qty;
|
||||
|
||||
if self.config.quote_quantity_limit || constraints.liquidity_limit_enabled {
|
||||
let top_level_liquidity = match (quote, side) {
|
||||
(Some(quote), OrderSide::Buy) => quote
|
||||
.ask1_volume
|
||||
.saturating_mul(round_lot.max(1) as u64)
|
||||
.min(u32::MAX as u64) as u32,
|
||||
(Some(quote), OrderSide::Sell) => quote
|
||||
.bid1_volume
|
||||
.saturating_mul(round_lot.max(1) as u64)
|
||||
.min(u32::MAX as u64) as u32,
|
||||
(None, OrderSide::Buy) => market.liquidity_for_buy().min(u32::MAX as u64) as u32,
|
||||
(None, OrderSide::Sell) => market.liquidity_for_sell().min(u32::MAX as u64) as u32,
|
||||
};
|
||||
if top_level_liquidity == 0 {
|
||||
return None;
|
||||
}
|
||||
let liquidity_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
|
||||
top_level_liquidity
|
||||
} else {
|
||||
self.round_lot_quantity(
|
||||
top_level_liquidity,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
)
|
||||
};
|
||||
if liquidity_limited == 0 {
|
||||
return None;
|
||||
}
|
||||
max_fill = max_fill.min(liquidity_limited);
|
||||
}
|
||||
|
||||
if constraints.volume_limit_enabled {
|
||||
if market.minute_volume == 0 {
|
||||
return None;
|
||||
}
|
||||
let consumed_turnover = execution_state
|
||||
.intraday_turnover
|
||||
.get(symbol)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.saturating_add(current_fill_quantity);
|
||||
let raw_limit = ((market.minute_volume as f64) * constraints.volume_percent).floor()
|
||||
as i64
|
||||
- consumed_turnover as i64;
|
||||
if raw_limit <= 0 {
|
||||
return None;
|
||||
}
|
||||
let volume_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
|
||||
raw_limit as u32
|
||||
} else {
|
||||
self.round_lot_quantity(raw_limit as u32, minimum_order_quantity, order_step_size)
|
||||
};
|
||||
if volume_limited == 0 {
|
||||
return None;
|
||||
}
|
||||
max_fill = max_fill.min(volume_limited);
|
||||
}
|
||||
|
||||
Some(max_fill)
|
||||
}
|
||||
|
||||
fn projected_select_execution_fill(
|
||||
&self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
@@ -1595,24 +1678,28 @@ impl PlatformExprStrategy {
|
||||
let Some(raw_quote_price) = self.projected_quote_raw_price(quote, side) else {
|
||||
continue;
|
||||
};
|
||||
let available_qty = if self.config.quote_quantity_limit {
|
||||
match side {
|
||||
OrderSide::Buy => quote.ask1_volume,
|
||||
OrderSide::Sell => quote.bid1_volume,
|
||||
}
|
||||
.saturating_mul(round_lot.max(1) as u64)
|
||||
.min(u32::MAX as u64) as u32
|
||||
} else {
|
||||
requested_qty.saturating_sub(filled_qty)
|
||||
};
|
||||
if available_qty == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let remaining_qty = requested_qty.saturating_sub(filled_qty);
|
||||
if remaining_qty == 0 {
|
||||
break;
|
||||
}
|
||||
let available_qty = self
|
||||
.projected_market_fillable_quantity(
|
||||
market,
|
||||
Some(quote),
|
||||
symbol,
|
||||
side,
|
||||
remaining_qty,
|
||||
round_lot,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
allow_odd_lot_sell,
|
||||
filled_qty,
|
||||
execution_state,
|
||||
)
|
||||
.unwrap_or(0);
|
||||
if available_qty == 0 {
|
||||
break;
|
||||
}
|
||||
let mut take_qty = remaining_qty.min(available_qty);
|
||||
if !(side == OrderSide::Sell && allow_odd_lot_sell && take_qty == remaining_qty) {
|
||||
take_qty =
|
||||
@@ -1780,9 +1867,25 @@ impl PlatformExprStrategy {
|
||||
execution_time,
|
||||
) && ctx.data.execution_quotes_on(date, symbol).is_empty()
|
||||
{
|
||||
let fallback_quantity = self.projected_market_fillable_quantity(
|
||||
market,
|
||||
None,
|
||||
symbol,
|
||||
OrderSide::Sell,
|
||||
quantity,
|
||||
round_lot,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
sellable_qty >= current_qty,
|
||||
0,
|
||||
execution_state,
|
||||
)?;
|
||||
if fallback_quantity == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(ProjectedExecutionFill {
|
||||
price: self.projected_execution_price(market, OrderSide::Sell),
|
||||
quantity,
|
||||
quantity: fallback_quantity,
|
||||
next_cursor: date.and_time(
|
||||
execution_time.unwrap_or_else(|| self.intraday_execution_start_time()),
|
||||
) + Duration::seconds(1),
|
||||
@@ -2144,9 +2247,25 @@ impl PlatformExprStrategy {
|
||||
None,
|
||||
) && ctx.data.execution_quotes_on(date, symbol).is_empty()
|
||||
{
|
||||
let fallback_quantity = self.projected_market_fillable_quantity(
|
||||
ctx.data.market(date, symbol)?,
|
||||
None,
|
||||
symbol,
|
||||
OrderSide::Buy,
|
||||
quantity,
|
||||
round_lot,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
false,
|
||||
0,
|
||||
execution_state,
|
||||
)?;
|
||||
if fallback_quantity == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(ProjectedExecutionFill {
|
||||
price: sizing_price,
|
||||
quantity,
|
||||
quantity: fallback_quantity,
|
||||
next_cursor: date.and_time(self.intraday_execution_start_time())
|
||||
+ Duration::seconds(1),
|
||||
})
|
||||
@@ -9880,6 +9999,8 @@ mod tests {
|
||||
cfg.slippage_model = SlippageModel::PriceRatio(0.002);
|
||||
cfg.matching_type = MatchingType::MinuteLast;
|
||||
cfg.quote_quantity_limit = false;
|
||||
cfg.risk_config.trading_constraints.volume_limit_enabled = false;
|
||||
cfg.risk_config.trading_constraints.liquidity_limit_enabled = false;
|
||||
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time"));
|
||||
let strategy = PlatformExprStrategy::new(cfg);
|
||||
let quote = ctx
|
||||
@@ -9910,6 +10031,92 @@ mod tests {
|
||||
assert!((position.average_cost - 5.12022).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn platform_projected_market_fill_caps_volume_limit() {
|
||||
let date = d(2023, 5, 12);
|
||||
let symbol = "603176.SH";
|
||||
let market = DailyMarketSnapshot {
|
||||
date,
|
||||
symbol: symbol.to_string(),
|
||||
timestamp: None,
|
||||
day_open: 5.10,
|
||||
open: 5.10,
|
||||
high: 5.20,
|
||||
low: 5.00,
|
||||
close: 5.20,
|
||||
last_price: 5.11,
|
||||
bid1: 5.11,
|
||||
ask1: 5.12,
|
||||
prev_close: 5.00,
|
||||
volume: 1_000_000,
|
||||
minute_volume: 10_000,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 5.50,
|
||||
lower_limit: 4.50,
|
||||
price_tick: 0.01,
|
||||
};
|
||||
let quote = IntradayExecutionQuote {
|
||||
date,
|
||||
symbol: symbol.to_string(),
|
||||
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
|
||||
last_price: 5.11,
|
||||
bid1: 5.11,
|
||||
ask1: 5.12,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
volume_delta: 10_000,
|
||||
amount_delta: 51_100.0,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
};
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
cfg.quote_quantity_limit = false;
|
||||
cfg.risk_config.trading_constraints.volume_limit_enabled = true;
|
||||
cfg.risk_config.trading_constraints.liquidity_limit_enabled = false;
|
||||
cfg.risk_config.trading_constraints.volume_percent = 0.25;
|
||||
let strategy = PlatformExprStrategy::new(cfg);
|
||||
let mut execution_state = super::ProjectedExecutionState::default();
|
||||
|
||||
assert_eq!(
|
||||
strategy.projected_market_fillable_quantity(
|
||||
&market,
|
||||
Some("e),
|
||||
symbol,
|
||||
OrderSide::Buy,
|
||||
50_000,
|
||||
100,
|
||||
100,
|
||||
100,
|
||||
false,
|
||||
0,
|
||||
&execution_state,
|
||||
),
|
||||
Some(2_500)
|
||||
);
|
||||
|
||||
execution_state
|
||||
.intraday_turnover
|
||||
.insert(symbol.to_string(), 2_400);
|
||||
assert_eq!(
|
||||
strategy.projected_market_fillable_quantity(
|
||||
&market,
|
||||
Some("e),
|
||||
symbol,
|
||||
OrderSide::Buy,
|
||||
50_000,
|
||||
100,
|
||||
100,
|
||||
100,
|
||||
false,
|
||||
0,
|
||||
&execution_state,
|
||||
),
|
||||
Some(100)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn platform_aiquant_target_value_uses_scheduled_mark_for_delta() {
|
||||
let prev_date = d(2023, 5, 11);
|
||||
@@ -10482,6 +10689,8 @@ mod tests {
|
||||
cfg.signal_symbol = symbol.to_string();
|
||||
cfg.stop_loss_expr.clear();
|
||||
cfg.take_profit_expr = "1.07".to_string();
|
||||
cfg.risk_config.trading_constraints.volume_limit_enabled = false;
|
||||
cfg.risk_config.trading_constraints.liquidity_limit_enabled = false;
|
||||
let mut strategy = PlatformExprStrategy::new(cfg);
|
||||
|
||||
let decision = strategy.on_day(&ctx).expect("platform decision");
|
||||
@@ -10691,6 +10900,8 @@ mod tests {
|
||||
cfg.signal_symbol = symbol.to_string();
|
||||
cfg.stop_loss_expr.clear();
|
||||
cfg.take_profit_expr = "1.07".to_string();
|
||||
cfg.risk_config.trading_constraints.volume_limit_enabled = false;
|
||||
cfg.risk_config.trading_constraints.liquidity_limit_enabled = false;
|
||||
let mut strategy = PlatformExprStrategy::new(cfg);
|
||||
|
||||
let first_decision = strategy.on_day(&first_ctx).expect("first decision");
|
||||
@@ -10983,6 +11194,8 @@ mod tests {
|
||||
cfg.delayed_limit_open_exit_enabled = true;
|
||||
cfg.delayed_limit_open_exit_time = Some(NaiveTime::from_hms_opt(9, 31, 0).unwrap());
|
||||
cfg.skip_month_day_ranges = vec![(Some(2024), 1, 15, 31)];
|
||||
cfg.risk_config.trading_constraints.volume_limit_enabled = false;
|
||||
cfg.risk_config.trading_constraints.liquidity_limit_enabled = false;
|
||||
let mut strategy = PlatformExprStrategy::new(cfg);
|
||||
strategy
|
||||
.pending_highlimit_holdings
|
||||
@@ -14864,6 +15077,8 @@ mod tests {
|
||||
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).unwrap());
|
||||
cfg.commission_rate = Some(0.0003);
|
||||
cfg.minimum_commission = Some(5.0);
|
||||
cfg.risk_config.trading_constraints.volume_limit_enabled = false;
|
||||
cfg.risk_config.trading_constraints.liquidity_limit_enabled = false;
|
||||
let strategy = PlatformExprStrategy::new(cfg);
|
||||
let mut projected = portfolio.clone();
|
||||
let mut execution_state = super::ProjectedExecutionState::default();
|
||||
@@ -17217,6 +17432,9 @@ mod tests {
|
||||
.to_string();
|
||||
cfg.exposure_expr = "signal_close > 0 ? 1.0 : 0.5".to_string();
|
||||
cfg.buy_scale_expr = "touched_upper_limit ? 1.0 : 0.5".to_string();
|
||||
cfg.quote_quantity_limit = false;
|
||||
cfg.risk_config.trading_constraints.volume_limit_enabled = false;
|
||||
cfg.risk_config.trading_constraints.liquidity_limit_enabled = false;
|
||||
cfg.rank_expr = concat!(
|
||||
"(close / rolling_mean(\"close\", 2)) * 0.5",
|
||||
" + pct_change(\"close\", 1) * boost"
|
||||
@@ -20291,6 +20509,8 @@ mod tests {
|
||||
cfg.aiquant_transaction_cost = true;
|
||||
cfg.strict_value_budget = true;
|
||||
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap());
|
||||
cfg.risk_config.trading_constraints.volume_limit_enabled = false;
|
||||
cfg.risk_config.trading_constraints.liquidity_limit_enabled = false;
|
||||
let mut strategy = PlatformExprStrategy::new(cfg);
|
||||
strategy.rebalance_day_counter = 20;
|
||||
|
||||
|
||||
@@ -2034,7 +2034,6 @@ impl OmniMicroCapStrategy {
|
||||
Some(fill.quantity)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn projected_market_fillable_quantity(
|
||||
&self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
@@ -2046,18 +2045,19 @@ impl OmniMicroCapStrategy {
|
||||
minimum_order_quantity: u32,
|
||||
order_step_size: u32,
|
||||
allow_odd_lot_sell: bool,
|
||||
current_fill_quantity: u32,
|
||||
execution_state: &ProjectedExecutionState,
|
||||
) -> Option<u32> {
|
||||
if requested_qty == 0 {
|
||||
return Some(0);
|
||||
}
|
||||
let snapshot = ctx.data.market(date, symbol)?;
|
||||
if snapshot.minute_volume == 0 {
|
||||
let constraints = self.config.risk_config.trading_constraints;
|
||||
if constraints.volume_limit_enabled && snapshot.minute_volume == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut max_fill = requested_qty;
|
||||
let constraints = self.config.risk_config.trading_constraints;
|
||||
if constraints.liquidity_limit_enabled {
|
||||
let top_level_liquidity = match side {
|
||||
OrderSide::Buy => snapshot.liquidity_for_buy(),
|
||||
@@ -2079,7 +2079,12 @@ impl OmniMicroCapStrategy {
|
||||
max_fill = max_fill.min(liquidity_limited);
|
||||
}
|
||||
|
||||
let consumed_turnover = *execution_state.intraday_turnover.get(symbol).unwrap_or(&0);
|
||||
let consumed_turnover = execution_state
|
||||
.intraday_turnover
|
||||
.get(symbol)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.saturating_add(current_fill_quantity);
|
||||
if constraints.volume_limit_enabled {
|
||||
let raw_limit = ((snapshot.minute_volume as f64) * constraints.volume_percent).floor()
|
||||
as i64
|
||||
@@ -2129,6 +2134,23 @@ impl OmniMicroCapStrategy {
|
||||
return None;
|
||||
}
|
||||
|
||||
let requested_qty = self.projected_market_fillable_quantity(
|
||||
ctx,
|
||||
date,
|
||||
symbol,
|
||||
side,
|
||||
requested_qty,
|
||||
round_lot,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
allow_odd_lot_sell,
|
||||
0,
|
||||
execution_state,
|
||||
)?;
|
||||
if requested_qty == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(market) = ctx.data.market(date, symbol) {
|
||||
let execution_price = self.projected_execution_price(market, side);
|
||||
if execution_price.is_finite() && execution_price > 0.0 {
|
||||
@@ -2222,7 +2244,25 @@ impl OmniMicroCapStrategy {
|
||||
if remaining_qty == 0 {
|
||||
break;
|
||||
}
|
||||
let mut take_qty = remaining_qty.min(available_qty);
|
||||
let market_fillable_qty = self
|
||||
.projected_market_fillable_quantity(
|
||||
ctx,
|
||||
date,
|
||||
symbol,
|
||||
side,
|
||||
remaining_qty,
|
||||
round_lot,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
allow_odd_lot_sell,
|
||||
filled_qty,
|
||||
execution_state,
|
||||
)
|
||||
.unwrap_or(0);
|
||||
if market_fillable_qty == 0 {
|
||||
break;
|
||||
}
|
||||
let mut take_qty = remaining_qty.min(available_qty).min(market_fillable_qty);
|
||||
if !(side == OrderSide::Sell && allow_odd_lot_sell && take_qty == remaining_qty) {
|
||||
take_qty =
|
||||
self.round_lot_quantity(take_qty, minimum_order_quantity, order_step_size);
|
||||
|
||||
Reference in New Issue
Block a user