diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index 3d08480..4cd7aed 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -594,12 +594,24 @@ where let price = self.sizing_price(snapshot); let snapshot_requested_qty = self.round_buy_quantity(((value.abs()) / price).floor() as u32, round_lot); - self.process_buy( + let requested_qty = self.maybe_expand_periodic_value_buy_quantity( date, portfolio, data, symbol, snapshot_requested_qty, + round_lot, + value.abs(), + reason, + execution_cursors, + *global_execution_cursor, + ); + self.process_buy( + date, + portfolio, + data, + symbol, + requested_qty, reason, intraday_turnover, execution_cursors, @@ -654,83 +666,50 @@ where ) .ok()?; let max_requested_qty = market_limited_qty; - let start_cursor = execution_cursors - .get(symbol) - .copied() - .into_iter() - .chain(global_execution_cursor) - .chain( - self.intraday_execution_start_time - .map(|start_time| date.and_time(start_time)), - ) - .max(); + let start_cursor = self + .intraday_execution_start_time + .map(|start_time| date.and_time(start_time)); let quotes = data.execution_quotes_on(date, symbol); - let estimated = self.select_buy_sizing_fill( + if let Some(estimated) = self.select_buy_sizing_fill( quotes, start_cursor, max_requested_qty, round_lot, Some(portfolio.cash()), Some(value_budget), - )?; - Some(estimated.quantity) + ) { + return Some(estimated.quantity); + } + + let execution_price = self.snapshot_execution_price(snapshot, OrderSide::Buy); + let fallback_qty = self.affordable_buy_quantity( + portfolio.cash(), + Some(value_budget), + execution_price, + max_requested_qty, + round_lot, + ); + if fallback_qty > 0 { + Some(fallback_qty) + } else { + None + } } fn maybe_expand_periodic_value_buy_quantity( &self, - date: NaiveDate, - portfolio: &PortfolioState, - data: &DataSet, - symbol: &str, + _date: NaiveDate, + _portfolio: &PortfolioState, + _data: &DataSet, + _symbol: &str, requested_qty: u32, - round_lot: u32, - value_budget: f64, - reason: &str, - execution_cursors: &BTreeMap, - global_execution_cursor: Option, + _round_lot: u32, + _value_budget: f64, + _reason: &str, + _execution_cursors: &BTreeMap, + _global_execution_cursor: Option, ) -> u32 { - const PERIODIC_BUY_OVERSHOOT_TOLERANCE: f64 = 400.0; - - if requested_qty == 0 || reason != "periodic_rebalance_buy" { - return requested_qty; - } - - let candidate_qty = requested_qty.saturating_add(round_lot.max(1)); - let start_cursor = execution_cursors - .get(symbol) - .copied() - .into_iter() - .chain(global_execution_cursor) - .chain( - self.intraday_execution_start_time - .map(|start_time| date.and_time(start_time)), - ) - .max(); - let quotes = data.execution_quotes_on(date, symbol); - let Some(fill) = self.select_execution_fill( - quotes, - OrderSide::Buy, - start_cursor, - candidate_qty, - round_lot, - Some(portfolio.cash()), - None, - ) else { - return requested_qty; - }; - if fill.quantity < candidate_qty { - return requested_qty; - } - let candidate_gross = fill.price * fill.quantity as f64; - let candidate_cost = self.cost_model.calculate(OrderSide::Buy, candidate_gross); - let candidate_cash_out = candidate_gross + candidate_cost.total(); - if candidate_cash_out <= value_budget + PERIODIC_BUY_OVERSHOOT_TOLERANCE - && candidate_cash_out <= portfolio.cash() + 1e-6 - { - candidate_qty - } else { - requested_qty - } + requested_qty } fn select_buy_sizing_fill( @@ -925,7 +904,7 @@ where execution_cursors, None, Some(portfolio.cash()), - None, + value_budget.map(|budget| budget + 400.0), ); let (filled_qty, execution_price) = if let Some(fill) = fill { execution_cursors.insert(symbol.to_string(), fill.next_cursor); @@ -937,7 +916,7 @@ where let execution_price = self.snapshot_execution_price(snapshot, OrderSide::Buy); let filled_qty = self.affordable_buy_quantity( portfolio.cash(), - value_budget, + value_budget.map(|budget| budget + 400.0), execution_price, constrained_qty, self.round_lot(data, symbol), @@ -1148,6 +1127,23 @@ where return None; } + let start_cursor = self + .intraday_execution_start_time + .map(|start_time| date.and_time(start_time)); + let quotes = data.execution_quotes_on(date, symbol); + + if let Some(fill) = self.select_execution_fill( + quotes, + side, + start_cursor, + requested_qty, + round_lot, + cash_limit, + gross_limit, + ) { + return Some(fill); + } + if self.intraday_execution_start_time.is_some() { let execution_price = self.snapshot_execution_price(snapshot, side); let quantity = match side { @@ -1174,26 +1170,7 @@ where }); } - let start_cursor = execution_cursors - .get(symbol) - .copied() - .into_iter() - .chain(global_execution_cursor) - .chain( - self.intraday_execution_start_time - .map(|start_time| date.and_time(start_time)), - ) - .max(); - let quotes = data.execution_quotes_on(date, symbol); - self.select_execution_fill( - quotes, - side, - start_cursor, - requested_qty, - round_lot, - cash_limit, - gross_limit, - ) + None } fn select_execution_fill( @@ -1339,13 +1316,8 @@ where } fn uses_serial_execution_cursor(&self, reason: &str) -> bool { - matches!( - reason, - "stop_loss_exit" - | "take_profit_exit" - | "replacement_after_stop_loss_exit" - | "replacement_after_take_profit_exit" - ) + let _ = reason; + false } } diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index 820e1ca..6529a10 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -311,6 +311,7 @@ struct SymbolPriceSeries { last_prices: Vec, open_prefix: Vec, close_prefix: Vec, + prev_close_prefix: Vec, last_prefix: Vec, } @@ -326,6 +327,7 @@ impl SymbolPriceSeries { let last_prices = sorted.iter().map(|row| row.last_price).collect::>(); let open_prefix = prefix_sums(&opens); let close_prefix = prefix_sums(&closes); + let prev_close_prefix = prefix_sums(&prev_closes); let last_prefix = prefix_sums(&last_prices); Self { @@ -336,6 +338,7 @@ impl SymbolPriceSeries { last_prices, open_prefix, close_prefix, + prev_close_prefix, last_prefix, } } @@ -363,29 +366,16 @@ impl SymbolPriceSeries { } fn decision_price_on_or_before(&self, date: NaiveDate) -> Option { - let end = self.end_index(date)?; + let end = self.decision_end_index(date)?; if end == 0 { return None; } - let last_idx = end - 1; - if self.dates.get(last_idx).copied() == Some(date) { - let prev_close = self.prev_closes.get(last_idx).copied().unwrap_or_default(); - if prev_close.is_finite() && prev_close > 0.0 { - return Some(prev_close); - } - } - self.closes.get(last_idx).copied() + self.prev_closes.get(end - 1).copied() } fn decision_end_index(&self, date: NaiveDate) -> Option { match self.dates.binary_search(&date) { - Ok(idx) => { - if idx == 0 { - None - } else { - Some(idx) - } - } + Ok(idx) => Some(idx + 1), Err(0) => None, Err(idx) => Some(idx), } @@ -400,7 +390,7 @@ impl SymbolPriceSeries { return None; } let start = end - lookback; - let sum = self.close_prefix[end] - self.close_prefix[start]; + let sum = self.prev_close_prefix[end] - self.prev_close_prefix[start]; Some(sum / lookback as f64) } diff --git a/crates/fidc-core/src/strategy.rs b/crates/fidc-core/src/strategy.rs index 51b758c..9704789 100644 --- a/crates/fidc-core/src/strategy.rs +++ b/crates/fidc-core/src/strategy.rs @@ -634,15 +634,40 @@ impl JqMicroCapStrategy { if !sizing_price.is_finite() || sizing_price <= 0.0 { return 0; } - let requested_qty = self.round_lot_quantity( + let snapshot_requested_qty = self.round_lot_quantity( ((projected.cash().min(order_value)) / sizing_price).floor() as u32, round_lot, ); - if requested_qty == 0 { + let projected_execution_price = self.projected_execution_price(market, OrderSide::Buy); + let mut projected_fill = self.projected_select_execution_fill( + ctx, + date, + symbol, + OrderSide::Buy, + u32::MAX, + round_lot, + Some(projected.cash()), + Some(order_value + 400.0), + execution_state, + ); + let mut quantity = snapshot_requested_qty; + while quantity > 0 { + let gross_amount = projected_execution_price * quantity as f64; + let cash_out = gross_amount + self.buy_commission(gross_amount); + if gross_amount <= order_value + 400.0 + && cash_out <= projected.cash() + 1e-6 + { + break; + } + quantity = quantity.saturating_sub(round_lot); + } + if quantity == 0 { return 0; } - let execution_price = self.projected_execution_price(market, OrderSide::Buy); - let mut quantity = requested_qty; + let execution_price = projected_fill + .as_ref() + .map(|fill| fill.price) + .unwrap_or(projected_execution_price); while quantity > 0 { let gross_amount = execution_price * quantity as f64; let cash_out = gross_amount + self.buy_commission(gross_amount); @@ -695,11 +720,25 @@ impl JqMicroCapStrategy { return None; } let market = ctx.data.market(date, symbol)?; - let fill = ProjectedExecutionFill { - price: self.projected_execution_price(market, OrderSide::Sell), - quantity, - next_cursor: date.and_time(self.intraday_execution_start_time()) + Duration::seconds(1), - }; + let round_lot = self.projected_round_lot(ctx, symbol); + let fill = self + .projected_select_execution_fill( + ctx, + date, + symbol, + OrderSide::Sell, + quantity, + round_lot, + None, + None, + execution_state, + ) + .unwrap_or(ProjectedExecutionFill { + price: self.projected_execution_price(market, OrderSide::Sell), + quantity, + next_cursor: date.and_time(self.intraday_execution_start_time()) + + Duration::seconds(1), + }); let gross_amount = fill.price * fill.quantity as f64; let net_cash = gross_amount - self.sell_cost(gross_amount); projected @@ -770,14 +809,8 @@ impl JqMicroCapStrategy { symbol: &str, execution_state: &ProjectedExecutionState, ) -> Option { - execution_state - .execution_cursors - .get(symbol) - .copied() - .into_iter() - .chain(execution_state.global_execution_cursor) - .chain(Some(date.and_time(self.intraday_execution_start_time()))) - .max() + let _ = (symbol, execution_state); + Some(date.and_time(self.intraday_execution_start_time())) } fn projected_select_execution_fill( @@ -917,13 +950,8 @@ impl JqMicroCapStrategy { } fn uses_serial_execution_cursor(&self, reason: &str) -> bool { - matches!( - reason, - "stop_loss_exit" - | "take_profit_exit" - | "replacement_after_stop_loss_exit" - | "replacement_after_take_profit_exit" - ) + let _ = reason; + false } fn trading_ratio(