Compare commits
1 Commits
v2026.05.1
...
v2026.05.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94662b6e75 |
@@ -2919,6 +2919,7 @@ where
|
||||
let order_step_size = self.order_step_size(data, symbol);
|
||||
let price = self.sizing_price(snapshot);
|
||||
let snapshot_requested_qty = self.value_buy_quantity(
|
||||
date,
|
||||
value.abs(),
|
||||
price,
|
||||
minimum_order_quantity,
|
||||
@@ -3014,6 +3015,7 @@ where
|
||||
let order_step_size = self.order_step_size(data, symbol);
|
||||
let price = self.sizing_price(snapshot);
|
||||
let snapshot_requested_qty = self.value_buy_quantity(
|
||||
date,
|
||||
value.abs(),
|
||||
price,
|
||||
minimum_order_quantity,
|
||||
@@ -3181,6 +3183,7 @@ where
|
||||
let order_step_size = self.order_step_size(data, symbol);
|
||||
let price = self.sizing_price(snapshot);
|
||||
let snapshot_requested_qty = self.value_buy_quantity(
|
||||
date,
|
||||
value.abs(),
|
||||
price,
|
||||
minimum_order_quantity,
|
||||
@@ -4066,6 +4069,7 @@ where
|
||||
|
||||
fn value_buy_quantity(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
value_budget: f64,
|
||||
price: f64,
|
||||
minimum_order_quantity: u32,
|
||||
@@ -4075,13 +4079,17 @@ where
|
||||
return 0;
|
||||
}
|
||||
let minimum = minimum_order_quantity.max(1);
|
||||
let step = order_step_size.max(1);
|
||||
if price * minimum as f64 > value_budget + 1e-6 {
|
||||
return 0;
|
||||
let raw_quantity = (value_budget / price).floor() as u32;
|
||||
let mut quantity =
|
||||
self.round_buy_quantity(raw_quantity, minimum_order_quantity, order_step_size);
|
||||
while quantity >= minimum {
|
||||
if self.estimated_buy_cash_out(date, price, quantity) <= value_budget + 1e-6 {
|
||||
return quantity;
|
||||
}
|
||||
let raw_steps = (value_budget / price / step as f64).round();
|
||||
let requested = ((raw_steps.max(1.0) as u32) * step).max(minimum);
|
||||
self.round_buy_quantity(requested, minimum_order_quantity, order_step_size)
|
||||
quantity =
|
||||
self.decrement_order_quantity(quantity, minimum_order_quantity, order_step_size);
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn decrement_order_quantity(
|
||||
@@ -4364,13 +4372,15 @@ where
|
||||
return None;
|
||||
}
|
||||
|
||||
let quote_quantity_limited = self.quote_quantity_limited(matching_type);
|
||||
let lot = round_lot.max(1);
|
||||
let eligible_quotes: Vec<&IntradayExecutionQuote> = quotes
|
||||
.iter()
|
||||
.filter(|quote| {
|
||||
!start_cursor.is_some_and(|cursor| quote.timestamp < cursor)
|
||||
&& !end_cursor.is_some_and(|cursor| quote.timestamp > cursor)
|
||||
&& self.quote_has_executable_liquidity(quote, side, matching_type)
|
||||
&& (!quote_quantity_limited
|
||||
|| self.quote_has_executable_liquidity(quote, side, matching_type))
|
||||
})
|
||||
.collect();
|
||||
let mut filled_qty = 0_u32;
|
||||
@@ -4398,21 +4408,26 @@ where
|
||||
continue;
|
||||
}
|
||||
let quote_price = self.execution_price_with_limit_slippage(quote_price, limit_price);
|
||||
let top_level_liquidity = match side {
|
||||
OrderSide::Buy => quote.ask1_volume,
|
||||
OrderSide::Sell => quote.bid1_volume,
|
||||
};
|
||||
let available_qty = top_level_liquidity
|
||||
.saturating_mul(lot as u64)
|
||||
.min(u32::MAX as u64) as u32;
|
||||
if available_qty == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let remaining_qty = requested_qty.saturating_sub(filled_qty);
|
||||
if remaining_qty == 0 {
|
||||
break;
|
||||
}
|
||||
let available_qty = if quote_quantity_limited {
|
||||
let top_level_liquidity = match side {
|
||||
OrderSide::Buy => quote.ask1_volume,
|
||||
OrderSide::Sell => quote.bid1_volume,
|
||||
};
|
||||
top_level_liquidity
|
||||
.saturating_mul(lot as u64)
|
||||
.min(u32::MAX as u64) as u32
|
||||
} else {
|
||||
remaining_qty
|
||||
};
|
||||
if available_qty == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut take_qty = if matching_type == MatchingType::Twap {
|
||||
let remaining_quotes = (eligible_quotes.len() - quote_index) as u32;
|
||||
let scheduled_qty =
|
||||
@@ -4514,6 +4529,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn quote_quantity_limited(&self, matching_type: MatchingType) -> bool {
|
||||
self.volume_limit || self.liquidity_limit || matching_type != MatchingType::NextTickLast
|
||||
}
|
||||
|
||||
fn uses_serial_execution_cursor(&self, reason: &str) -> bool {
|
||||
let _ = reason;
|
||||
false
|
||||
@@ -4579,3 +4598,35 @@ fn sell_reason(decision: &StrategyDecision, symbol: &str) -> &'static str {
|
||||
"rebalance_sell"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{BrokerSimulator, MatchingType};
|
||||
use crate::cost::ChinaAShareCostModel;
|
||||
use crate::rules::ChinaEquityRuleHooks;
|
||||
|
||||
#[test]
|
||||
fn next_tick_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() {
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false);
|
||||
|
||||
assert!(!broker.quote_quantity_limited(MatchingType::NextTickLast));
|
||||
assert!(broker.quote_quantity_limited(MatchingType::CounterpartyOffer));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_tick_last_keeps_quote_quantity_cap_when_limits_enabled() {
|
||||
let volume_limited =
|
||||
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_volume_limit(true)
|
||||
.with_liquidity_limit(false);
|
||||
let liquidity_limited =
|
||||
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(true);
|
||||
|
||||
assert!(volume_limited.quote_quantity_limited(MatchingType::NextTickLast));
|
||||
assert!(liquidity_limited.quote_quantity_limited(MatchingType::NextTickLast));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ pub struct ChinaAShareCostModel {
|
||||
impl Default for ChinaAShareCostModel {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
commission_rate: 0.0003,
|
||||
commission_rate: 0.0008,
|
||||
stamp_tax_rate_before_change: 0.001,
|
||||
stamp_tax_rate_after_change: 0.0005,
|
||||
minimum_commission: 5.0,
|
||||
|
||||
@@ -822,13 +822,18 @@ impl PlatformExprStrategy {
|
||||
return 0;
|
||||
}
|
||||
let minimum = minimum_order_quantity.max(1);
|
||||
let step = order_step_size.max(1);
|
||||
if price * minimum as f64 > value_budget + 1e-6 {
|
||||
return 0;
|
||||
let raw_quantity = (value_budget / price).floor() as u32;
|
||||
let mut quantity =
|
||||
self.round_lot_quantity(raw_quantity, minimum_order_quantity, order_step_size);
|
||||
while quantity >= minimum {
|
||||
let gross_amount = price * quantity as f64;
|
||||
if gross_amount + self.buy_commission(gross_amount) <= value_budget + 1e-6 {
|
||||
return quantity;
|
||||
}
|
||||
let raw_steps = (value_budget / price / step as f64).round();
|
||||
let requested = ((raw_steps.max(1.0) as u32) * step).max(minimum);
|
||||
self.round_lot_quantity(requested, minimum_order_quantity, order_step_size)
|
||||
quantity =
|
||||
self.decrement_order_quantity(quantity, minimum_order_quantity, order_step_size);
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn decrement_order_quantity(
|
||||
@@ -5093,6 +5098,16 @@ mod tests {
|
||||
assert!(!rewritten.contains('?'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn platform_order_value_quantity_includes_buy_commission_budget() {
|
||||
let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation());
|
||||
|
||||
assert_eq!(strategy.value_buy_quantity(4_000.0, 20.38, 100, 100), 100);
|
||||
assert_eq!(strategy.value_buy_quantity(4_000.0, 15.20, 100, 100), 200);
|
||||
assert_eq!(strategy.value_buy_quantity(4_000.0, 28.85, 100, 100), 100);
|
||||
assert_eq!(strategy.value_buy_quantity(4_000.0, 37.40, 100, 100), 100);
|
||||
}
|
||||
|
||||
fn sample_calendar() -> TradingCalendar {
|
||||
TradingCalendar::new(vec![
|
||||
d(2025, 1, 30),
|
||||
|
||||
@@ -61,7 +61,7 @@ fn china_cost_model_applies_minimum_commission_and_stamp_tax() {
|
||||
assert_eq!(buy.stamp_tax, 0.0);
|
||||
|
||||
let sell = model.calculate(d(2023, 8, 25), OrderSide::Sell, 100_000.0);
|
||||
assert!((sell.commission - 30.0).abs() < 1e-9);
|
||||
assert!((sell.commission - 80.0).abs() < 1e-9);
|
||||
assert!((sell.stamp_tax - 100.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ fn china_cost_model_tracks_minimum_commission_per_order_id() {
|
||||
|
||||
assert!((first.commission - 5.0).abs() < 1e-9);
|
||||
assert!(second.commission.abs() < 1e-9);
|
||||
assert!((third.commission - 1.6).abs() < 1e-9);
|
||||
assert!((third.commission - 12.6).abs() < 1e-9);
|
||||
assert!((another_order.commission - 5.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user