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 order_step_size = self.order_step_size(data, symbol);
|
||||||
let price = self.sizing_price(snapshot);
|
let price = self.sizing_price(snapshot);
|
||||||
let snapshot_requested_qty = self.value_buy_quantity(
|
let snapshot_requested_qty = self.value_buy_quantity(
|
||||||
|
date,
|
||||||
value.abs(),
|
value.abs(),
|
||||||
price,
|
price,
|
||||||
minimum_order_quantity,
|
minimum_order_quantity,
|
||||||
@@ -3014,6 +3015,7 @@ where
|
|||||||
let order_step_size = self.order_step_size(data, symbol);
|
let order_step_size = self.order_step_size(data, symbol);
|
||||||
let price = self.sizing_price(snapshot);
|
let price = self.sizing_price(snapshot);
|
||||||
let snapshot_requested_qty = self.value_buy_quantity(
|
let snapshot_requested_qty = self.value_buy_quantity(
|
||||||
|
date,
|
||||||
value.abs(),
|
value.abs(),
|
||||||
price,
|
price,
|
||||||
minimum_order_quantity,
|
minimum_order_quantity,
|
||||||
@@ -3181,6 +3183,7 @@ where
|
|||||||
let order_step_size = self.order_step_size(data, symbol);
|
let order_step_size = self.order_step_size(data, symbol);
|
||||||
let price = self.sizing_price(snapshot);
|
let price = self.sizing_price(snapshot);
|
||||||
let snapshot_requested_qty = self.value_buy_quantity(
|
let snapshot_requested_qty = self.value_buy_quantity(
|
||||||
|
date,
|
||||||
value.abs(),
|
value.abs(),
|
||||||
price,
|
price,
|
||||||
minimum_order_quantity,
|
minimum_order_quantity,
|
||||||
@@ -4066,6 +4069,7 @@ where
|
|||||||
|
|
||||||
fn value_buy_quantity(
|
fn value_buy_quantity(
|
||||||
&self,
|
&self,
|
||||||
|
date: NaiveDate,
|
||||||
value_budget: f64,
|
value_budget: f64,
|
||||||
price: f64,
|
price: f64,
|
||||||
minimum_order_quantity: u32,
|
minimum_order_quantity: u32,
|
||||||
@@ -4075,13 +4079,17 @@ where
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let minimum = minimum_order_quantity.max(1);
|
let minimum = minimum_order_quantity.max(1);
|
||||||
let step = order_step_size.max(1);
|
let raw_quantity = (value_budget / price).floor() as u32;
|
||||||
if price * minimum as f64 > value_budget + 1e-6 {
|
let mut quantity =
|
||||||
return 0;
|
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;
|
||||||
|
}
|
||||||
|
quantity =
|
||||||
|
self.decrement_order_quantity(quantity, minimum_order_quantity, order_step_size);
|
||||||
}
|
}
|
||||||
let raw_steps = (value_budget / price / step as f64).round();
|
0
|
||||||
let requested = ((raw_steps.max(1.0) as u32) * step).max(minimum);
|
|
||||||
self.round_buy_quantity(requested, minimum_order_quantity, order_step_size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrement_order_quantity(
|
fn decrement_order_quantity(
|
||||||
@@ -4364,13 +4372,15 @@ where
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let quote_quantity_limited = self.quote_quantity_limited(matching_type);
|
||||||
let lot = round_lot.max(1);
|
let lot = round_lot.max(1);
|
||||||
let eligible_quotes: Vec<&IntradayExecutionQuote> = quotes
|
let eligible_quotes: Vec<&IntradayExecutionQuote> = quotes
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|quote| {
|
.filter(|quote| {
|
||||||
!start_cursor.is_some_and(|cursor| quote.timestamp < cursor)
|
!start_cursor.is_some_and(|cursor| quote.timestamp < cursor)
|
||||||
&& !end_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();
|
.collect();
|
||||||
let mut filled_qty = 0_u32;
|
let mut filled_qty = 0_u32;
|
||||||
@@ -4398,21 +4408,26 @@ where
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let quote_price = self.execution_price_with_limit_slippage(quote_price, limit_price);
|
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);
|
let remaining_qty = requested_qty.saturating_sub(filled_qty);
|
||||||
if remaining_qty == 0 {
|
if remaining_qty == 0 {
|
||||||
break;
|
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 mut take_qty = if matching_type == MatchingType::Twap {
|
||||||
let remaining_quotes = (eligible_quotes.len() - quote_index) as u32;
|
let remaining_quotes = (eligible_quotes.len() - quote_index) as u32;
|
||||||
let scheduled_qty =
|
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 {
|
fn uses_serial_execution_cursor(&self, reason: &str) -> bool {
|
||||||
let _ = reason;
|
let _ = reason;
|
||||||
false
|
false
|
||||||
@@ -4579,3 +4598,35 @@ fn sell_reason(decision: &StrategyDecision, symbol: &str) -> &'static str {
|
|||||||
"rebalance_sell"
|
"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 {
|
impl Default for ChinaAShareCostModel {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
commission_rate: 0.0003,
|
commission_rate: 0.0008,
|
||||||
stamp_tax_rate_before_change: 0.001,
|
stamp_tax_rate_before_change: 0.001,
|
||||||
stamp_tax_rate_after_change: 0.0005,
|
stamp_tax_rate_after_change: 0.0005,
|
||||||
minimum_commission: 5.0,
|
minimum_commission: 5.0,
|
||||||
|
|||||||
@@ -822,13 +822,18 @@ impl PlatformExprStrategy {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let minimum = minimum_order_quantity.max(1);
|
let minimum = minimum_order_quantity.max(1);
|
||||||
let step = order_step_size.max(1);
|
let raw_quantity = (value_budget / price).floor() as u32;
|
||||||
if price * minimum as f64 > value_budget + 1e-6 {
|
let mut quantity =
|
||||||
return 0;
|
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;
|
||||||
|
}
|
||||||
|
quantity =
|
||||||
|
self.decrement_order_quantity(quantity, minimum_order_quantity, order_step_size);
|
||||||
}
|
}
|
||||||
let raw_steps = (value_budget / price / step as f64).round();
|
0
|
||||||
let requested = ((raw_steps.max(1.0) as u32) * step).max(minimum);
|
|
||||||
self.round_lot_quantity(requested, minimum_order_quantity, order_step_size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrement_order_quantity(
|
fn decrement_order_quantity(
|
||||||
@@ -5093,6 +5098,16 @@ mod tests {
|
|||||||
assert!(!rewritten.contains('?'));
|
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 {
|
fn sample_calendar() -> TradingCalendar {
|
||||||
TradingCalendar::new(vec![
|
TradingCalendar::new(vec![
|
||||||
d(2025, 1, 30),
|
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);
|
assert_eq!(buy.stamp_tax, 0.0);
|
||||||
|
|
||||||
let sell = model.calculate(d(2023, 8, 25), OrderSide::Sell, 100_000.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);
|
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!((first.commission - 5.0).abs() < 1e-9);
|
||||||
assert!(second.commission.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);
|
assert!((another_order.commission - 5.0).abs() < 1e-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user