Align projected stock order sizing semantics
This commit is contained in:
@@ -175,7 +175,11 @@ impl CnSmallCapRotationStrategy {
|
||||
let (sum, count) = window.fold((0.0, 0usize), |(sum, count), value| {
|
||||
(sum + value, count + 1)
|
||||
});
|
||||
if count == 0 { 0.0 } else { sum / count as f64 }
|
||||
if count == 0 {
|
||||
0.0
|
||||
} else {
|
||||
sum / count as f64
|
||||
}
|
||||
}
|
||||
|
||||
fn gross_exposure(&self, closes: &[f64]) -> f64 {
|
||||
@@ -325,7 +329,7 @@ impl Strategy for CnSmallCapRotationStrategy {
|
||||
order_intents: Vec::new(),
|
||||
notes: vec![format!("warmup: {}", message)],
|
||||
diagnostics: vec![
|
||||
"insufficient history; skip trading on warmup dates".to_string(),
|
||||
"insufficient history; skip trading on warmup dates".to_string()
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -591,12 +595,42 @@ impl JqMicroCapStrategy {
|
||||
|
||||
fn sell_cost(&self, date: NaiveDate, gross_amount: f64) -> f64 {
|
||||
let model = ChinaAShareCostModel::default();
|
||||
model.commission_for(gross_amount) + model.stamp_tax_for(date, OrderSide::Sell, gross_amount)
|
||||
model.commission_for(gross_amount)
|
||||
+ model.stamp_tax_for(date, OrderSide::Sell, gross_amount)
|
||||
}
|
||||
|
||||
fn round_lot_quantity(&self, quantity: u32, round_lot: u32) -> u32 {
|
||||
let lot = round_lot.max(1);
|
||||
(quantity / lot) * lot
|
||||
fn round_lot_quantity(
|
||||
&self,
|
||||
quantity: u32,
|
||||
minimum_order_quantity: u32,
|
||||
order_step_size: u32,
|
||||
) -> u32 {
|
||||
let step = order_step_size.max(1);
|
||||
let normalized = (quantity / step) * step;
|
||||
if normalized < minimum_order_quantity.max(1) {
|
||||
0
|
||||
} else {
|
||||
normalized
|
||||
}
|
||||
}
|
||||
|
||||
fn decrement_order_quantity(
|
||||
&self,
|
||||
quantity: u32,
|
||||
minimum_order_quantity: u32,
|
||||
order_step_size: u32,
|
||||
) -> u32 {
|
||||
let minimum = minimum_order_quantity.max(1);
|
||||
if quantity <= minimum {
|
||||
0
|
||||
} else {
|
||||
let next = quantity.saturating_sub(order_step_size.max(1));
|
||||
if next < minimum {
|
||||
0
|
||||
} else {
|
||||
next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn intraday_execution_start_time(&self) -> NaiveTime {
|
||||
@@ -611,17 +645,33 @@ impl JqMicroCapStrategy {
|
||||
.max(1)
|
||||
}
|
||||
|
||||
fn projected_minimum_order_quantity(&self, ctx: &StrategyContext<'_>, symbol: &str) -> u32 {
|
||||
ctx.data
|
||||
.instrument(symbol)
|
||||
.map(|instrument| instrument.minimum_order_quantity())
|
||||
.unwrap_or(100)
|
||||
.max(1)
|
||||
}
|
||||
|
||||
fn projected_order_step_size(&self, ctx: &StrategyContext<'_>, symbol: &str) -> u32 {
|
||||
ctx.data
|
||||
.instrument(symbol)
|
||||
.map(|instrument| instrument.order_step_size())
|
||||
.unwrap_or(100)
|
||||
.max(1)
|
||||
}
|
||||
|
||||
fn projected_buy_quantity(&self, cash: f64, sizing_price: f64, execution_price: f64) -> u32 {
|
||||
if cash <= 0.0 || sizing_price <= 0.0 || execution_price <= 0.0 {
|
||||
return 0;
|
||||
}
|
||||
let mut quantity = self.round_lot_quantity((cash / sizing_price).floor() as u32, 100);
|
||||
let mut quantity = self.round_lot_quantity((cash / sizing_price).floor() as u32, 100, 100);
|
||||
while quantity > 0 {
|
||||
let gross_amount = execution_price * quantity as f64;
|
||||
if gross_amount + self.buy_commission(gross_amount) <= cash + 1e-6 {
|
||||
return quantity;
|
||||
}
|
||||
quantity = quantity.saturating_sub(100);
|
||||
quantity = self.decrement_order_quantity(quantity, 100, 100);
|
||||
}
|
||||
0
|
||||
}
|
||||
@@ -649,6 +699,8 @@ impl JqMicroCapStrategy {
|
||||
return 0;
|
||||
}
|
||||
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 market = match ctx.data.market(date, symbol) {
|
||||
Some(market) => market,
|
||||
None => return 0,
|
||||
@@ -659,7 +711,8 @@ impl JqMicroCapStrategy {
|
||||
}
|
||||
let snapshot_requested_qty = self.round_lot_quantity(
|
||||
((projected.cash().min(order_value)) / sizing_price).floor() as u32,
|
||||
round_lot,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
);
|
||||
let projected_execution_price = self.projected_execution_price(market, OrderSide::Buy);
|
||||
let projected_fill = self.projected_select_execution_fill(
|
||||
@@ -669,6 +722,9 @@ impl JqMicroCapStrategy {
|
||||
OrderSide::Buy,
|
||||
u32::MAX,
|
||||
round_lot,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
false,
|
||||
Some(projected.cash()),
|
||||
Some(order_value + 400.0),
|
||||
execution_state,
|
||||
@@ -676,12 +732,11 @@ impl JqMicroCapStrategy {
|
||||
let mut quantity = snapshot_requested_qty;
|
||||
while quantity > 0 {
|
||||
let gross_amount = projected_execution_price * quantity as f64;
|
||||
if gross_amount <= order_value + 400.0
|
||||
&& gross_amount <= projected.cash() + 1e-6
|
||||
{
|
||||
if gross_amount <= order_value + 400.0 && gross_amount <= projected.cash() + 1e-6 {
|
||||
break;
|
||||
}
|
||||
quantity = quantity.saturating_sub(round_lot);
|
||||
quantity =
|
||||
self.decrement_order_quantity(quantity, minimum_order_quantity, order_step_size);
|
||||
}
|
||||
if quantity == 0 {
|
||||
return 0;
|
||||
@@ -695,7 +750,8 @@ impl JqMicroCapStrategy {
|
||||
if gross_amount <= projected.cash() + 1e-6 {
|
||||
break;
|
||||
}
|
||||
quantity = quantity.saturating_sub(round_lot);
|
||||
quantity =
|
||||
self.decrement_order_quantity(quantity, minimum_order_quantity, order_step_size);
|
||||
}
|
||||
if quantity == 0 {
|
||||
return 0;
|
||||
@@ -742,6 +798,8 @@ impl JqMicroCapStrategy {
|
||||
}
|
||||
let market = ctx.data.market(date, symbol)?;
|
||||
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 fill = self
|
||||
.projected_select_execution_fill(
|
||||
ctx,
|
||||
@@ -750,6 +808,9 @@ impl JqMicroCapStrategy {
|
||||
OrderSide::Sell,
|
||||
quantity,
|
||||
round_lot,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
execution_state,
|
||||
@@ -788,7 +849,10 @@ impl JqMicroCapStrategy {
|
||||
symbol: &str,
|
||||
side: OrderSide,
|
||||
requested_qty: u32,
|
||||
round_lot: u32,
|
||||
_round_lot: u32,
|
||||
minimum_order_quantity: u32,
|
||||
order_step_size: u32,
|
||||
allow_odd_lot_sell: bool,
|
||||
execution_state: &ProjectedExecutionState,
|
||||
) -> Option<u32> {
|
||||
if requested_qty == 0 {
|
||||
@@ -799,7 +863,6 @@ impl JqMicroCapStrategy {
|
||||
return None;
|
||||
}
|
||||
|
||||
let lot = round_lot.max(1);
|
||||
let mut max_fill = requested_qty;
|
||||
let top_level_liquidity = match side {
|
||||
OrderSide::Buy => snapshot.liquidity_for_buy(),
|
||||
@@ -809,7 +872,12 @@ impl JqMicroCapStrategy {
|
||||
if top_level_liquidity == 0 {
|
||||
return None;
|
||||
}
|
||||
max_fill = max_fill.min(self.round_lot_quantity(top_level_liquidity, lot));
|
||||
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)
|
||||
};
|
||||
max_fill = max_fill.min(liquidity_limited);
|
||||
|
||||
let consumed_turnover = *execution_state.intraday_turnover.get(symbol).unwrap_or(&0);
|
||||
let raw_limit =
|
||||
@@ -817,7 +885,11 @@ impl JqMicroCapStrategy {
|
||||
if raw_limit <= 0 {
|
||||
return None;
|
||||
}
|
||||
let volume_limited = self.round_lot_quantity(raw_limit as u32, lot);
|
||||
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;
|
||||
}
|
||||
@@ -842,6 +914,9 @@ impl JqMicroCapStrategy {
|
||||
side: OrderSide,
|
||||
requested_qty: u32,
|
||||
round_lot: u32,
|
||||
minimum_order_quantity: u32,
|
||||
order_step_size: u32,
|
||||
allow_odd_lot_sell: bool,
|
||||
cash_limit: Option<f64>,
|
||||
gross_limit: Option<f64>,
|
||||
execution_state: &ProjectedExecutionState,
|
||||
@@ -856,26 +931,39 @@ impl JqMicroCapStrategy {
|
||||
let quantity = match side {
|
||||
OrderSide::Buy => {
|
||||
let cash = cash_limit.unwrap_or(f64::INFINITY);
|
||||
let mut take_qty = self.round_lot_quantity(requested_qty, round_lot.max(1));
|
||||
let mut take_qty = self.round_lot_quantity(
|
||||
requested_qty,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
);
|
||||
while take_qty > 0 {
|
||||
let candidate_gross = execution_price * take_qty as f64;
|
||||
if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
|
||||
take_qty = take_qty.saturating_sub(round_lot.max(1));
|
||||
take_qty = self.decrement_order_quantity(
|
||||
take_qty,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let candidate_cash = candidate_gross + self.buy_commission(candidate_gross);
|
||||
let candidate_cash =
|
||||
candidate_gross + self.buy_commission(candidate_gross);
|
||||
if candidate_cash <= cash + 1e-6 {
|
||||
break;
|
||||
}
|
||||
take_qty = take_qty.saturating_sub(round_lot.max(1));
|
||||
take_qty = self.decrement_order_quantity(
|
||||
take_qty,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
);
|
||||
}
|
||||
take_qty
|
||||
}
|
||||
OrderSide::Sell => requested_qty,
|
||||
};
|
||||
if quantity > 0 {
|
||||
let next_cursor = date.and_time(self.intraday_execution_start_time())
|
||||
+ Duration::seconds(1);
|
||||
let next_cursor =
|
||||
date.and_time(self.intraday_execution_start_time()) + Duration::seconds(1);
|
||||
return Some(ProjectedExecutionFill {
|
||||
price: execution_price,
|
||||
quantity,
|
||||
@@ -885,7 +973,6 @@ impl JqMicroCapStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
let lot = round_lot.max(1);
|
||||
let start_cursor = self.projected_execution_start_cursor(date, symbol, execution_state);
|
||||
let quotes = ctx.data.execution_quotes_on(date, symbol);
|
||||
let mut filled_qty = 0_u32;
|
||||
@@ -921,7 +1008,7 @@ impl JqMicroCapStrategy {
|
||||
OrderSide::Buy => quote.ask1_volume,
|
||||
OrderSide::Sell => quote.bid1_volume,
|
||||
}
|
||||
.saturating_mul(lot as u64)
|
||||
.saturating_mul(round_lot.max(1) as u64)
|
||||
.min(u32::MAX as u64) as u32;
|
||||
if available_qty == 0 {
|
||||
continue;
|
||||
@@ -931,7 +1018,11 @@ impl JqMicroCapStrategy {
|
||||
if remaining_qty == 0 {
|
||||
break;
|
||||
}
|
||||
let mut take_qty = self.round_lot_quantity(remaining_qty.min(available_qty), lot);
|
||||
let mut take_qty = remaining_qty.min(available_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);
|
||||
}
|
||||
if take_qty == 0 {
|
||||
continue;
|
||||
}
|
||||
@@ -940,13 +1031,21 @@ impl JqMicroCapStrategy {
|
||||
while take_qty > 0 {
|
||||
let candidate_gross = gross_amount + quote_price * take_qty as f64;
|
||||
if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
|
||||
take_qty = take_qty.saturating_sub(lot);
|
||||
take_qty = self.decrement_order_quantity(
|
||||
take_qty,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if candidate_gross + self.buy_commission(candidate_gross) <= cash + 1e-6 {
|
||||
break;
|
||||
}
|
||||
take_qty = take_qty.saturating_sub(lot);
|
||||
take_qty = self.decrement_order_quantity(
|
||||
take_qty,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
);
|
||||
}
|
||||
if take_qty == 0 {
|
||||
break;
|
||||
@@ -1436,7 +1535,7 @@ impl Strategy for JqMicroCapStrategy {
|
||||
order_intents: Vec::new(),
|
||||
notes: vec![format!("warmup: {}", message)],
|
||||
diagnostics: vec![
|
||||
"insufficient history; skip trading on warmup dates".to_string(),
|
||||
"insufficient history; skip trading on warmup dates".to_string()
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user