From d2c65c91b7a2f364257c777b9ff77249e956fa36 Mon Sep 17 00:00:00 2001 From: boris Date: Tue, 16 Jun 2026 06:22:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=B9=B3=E5=8F=B0=E7=AD=96?= =?UTF-8?q?=E7=95=A5=E6=8A=95=E5=BD=B1=E6=92=AE=E5=90=88=E4=BB=B7=E5=8F=A3?= =?UTF-8?q?=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 206 +++++++++++++++++- 1 file changed, 197 insertions(+), 9 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index aaf8190..08edcec 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -4,7 +4,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap}; use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime}; use rhai::{AST, Dynamic, Engine, Map, Scope}; -use crate::broker::SlippageModel; +use crate::broker::{MatchingType, SlippageModel}; use crate::cost::ChinaAShareCostModel; use crate::data::{ DailyMarketSnapshot, EligibleUniverseSnapshot, PriceField, decision_free_float_cap_bn, @@ -215,6 +215,7 @@ pub struct PlatformExprStrategyConfig { pub stamp_tax_rate_after_change: Option, pub strict_value_budget: bool, pub slippage_model: SlippageModel, + pub matching_type: MatchingType, pub quote_quantity_limit: bool, pub current_day_precomputed_factors: bool, pub intraday_execution_time: Option, @@ -282,6 +283,7 @@ fn band_low(index_close) { stamp_tax_rate_after_change: None, strict_value_budget: false, slippage_model: SlippageModel::None, + matching_type: MatchingType::NextTickLast, quote_quantity_limit: true, current_day_precomputed_factors: false, intraday_execution_time: None, @@ -1133,7 +1135,7 @@ impl PlatformExprStrategy { symbol: &str, ) -> Option { self.aiquant_scheduled_quote(ctx, date, symbol) - .and_then(|quote| quote.buy_price()) + .and_then(|quote| self.projected_quote_raw_price(quote, OrderSide::Buy)) } fn aiquant_scheduled_last_price( @@ -1211,6 +1213,41 @@ impl PlatformExprStrategy { bounded } + fn projected_quote_raw_price( + &self, + quote: &crate::data::IntradayExecutionQuote, + side: OrderSide, + ) -> Option { + let last = + || (quote.last_price.is_finite() && quote.last_price > 0.0).then_some(quote.last_price); + match self.config.matching_type { + MatchingType::NextTickBestOwn => match side { + OrderSide::Buy => (quote.bid1.is_finite() && quote.bid1 > 0.0) + .then_some(quote.bid1) + .or_else(last), + OrderSide::Sell => (quote.ask1.is_finite() && quote.ask1 > 0.0) + .then_some(quote.ask1) + .or_else(last), + }, + MatchingType::NextTickBestCounterparty | MatchingType::CounterpartyOffer => { + match side { + OrderSide::Buy => quote.buy_price(), + OrderSide::Sell => quote.sell_price(), + } + } + MatchingType::NextTickLast | MatchingType::Vwap | MatchingType::Twap => { + last().or_else(|| match side { + OrderSide::Buy => quote.buy_price(), + OrderSide::Sell => quote.sell_price(), + }) + } + _ => match side { + OrderSide::Buy => quote.buy_price(), + OrderSide::Sell => quote.sell_price(), + }, + } + } + fn projected_execution_limit_rejection_reason( market: &DailyMarketSnapshot, side: OrderSide, @@ -1303,10 +1340,7 @@ impl PlatformExprStrategy { let mut last_timestamp = None; for quote in selected_quotes { - let Some(raw_quote_price) = (match side { - OrderSide::Buy => quote.buy_price(), - OrderSide::Sell => quote.sell_price(), - }) else { + let Some(raw_quote_price) = self.projected_quote_raw_price(quote, side) else { continue; }; let available_qty = if self.config.quote_quantity_limit { @@ -6975,9 +7009,10 @@ mod tests { use crate::{ AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, FactorTextValue, FuturesCommissionType, - FuturesTradingParameter, Instrument, IntradayExecutionQuote, OpenOrderView, OrderIntent, - PortfolioState, ProcessEvent, ProcessEventKind, ScheduleStage, ScheduleTimeRule, Strategy, - StrategyContext, TargetPortfolioOrderPricing, TradingCalendar, default_stage_time, + FuturesTradingParameter, Instrument, IntradayExecutionQuote, MatchingType, OpenOrderView, + OrderIntent, OrderSide, PortfolioState, ProcessEvent, ProcessEventKind, ScheduleStage, + ScheduleTimeRule, SlippageModel, Strategy, StrategyContext, TargetPortfolioOrderPricing, + TradingCalendar, default_stage_time, }; fn d(year: i32, month: u32, day: u32) -> NaiveDate { @@ -7235,6 +7270,159 @@ mod tests { assert_eq!(strategy.value_buy_quantity(4_000.0, 37.40, 100, 100), 100); assert_eq!(strategy.value_buy_quantity(4_776.0, 11.93, 100, 100), 300); assert_eq!(strategy.value_buy_quantity(4_848.0, 11.93, 100, 100), 400); + + let mut aiquant_cfg = PlatformExprStrategyConfig::microcap_rotation(); + aiquant_cfg.aiquant_transaction_cost = true; + aiquant_cfg.commission_rate = Some(0.0003); + aiquant_cfg.minimum_commission = Some(5.0); + let aiquant_strategy = PlatformExprStrategy::new(aiquant_cfg); + assert_eq!( + aiquant_strategy.value_buy_quantity(125_000.0, 5.12022, 100, 100), + 24_400 + ); + assert_eq!( + aiquant_strategy.value_buy_quantity(125_000.0, 5.13024, 100, 100), + 24_300 + ); + } + + #[test] + fn platform_aiquant_next_tick_last_projection_uses_last_quote_for_buy_budget() { + let date = d(2023, 5, 4); + let symbol = "000782.SZ"; + let data = DataSet::from_components_with_actions_and_quotes( + vec![Instrument { + symbol: symbol.to_string(), + name: symbol.to_string(), + board: "SZ".to_string(), + round_lot: 100, + listed_at: Some(d(2010, 1, 1)), + delisted_at: None, + status: "active".to_string(), + }], + vec![DailyMarketSnapshot { + date, + symbol: symbol.to_string(), + timestamp: Some("2023-05-04 10:40:00".to_string()), + 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, + tick_volume: 10_000, + bid1_volume: 324, + ask1_volume: 1_717, + trading_phase: Some("continuous".to_string()), + paused: false, + upper_limit: 5.50, + lower_limit: 4.50, + price_tick: 0.01, + }], + vec![DailyFactorSnapshot { + date, + symbol: symbol.to_string(), + market_cap_bn: 10.0, + free_float_cap_bn: 10.0, + pe_ttm: 8.0, + turnover_ratio: Some(1.0), + effective_turnover_ratio: Some(1.0), + extra_factors: BTreeMap::new(), + }], + vec![CandidateEligibility { + date, + symbol: symbol.to_string(), + is_st: false, + is_new_listing: false, + is_paused: false, + allow_buy: true, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + }], + vec![BenchmarkSnapshot { + date, + benchmark: "000852.SH".to_string(), + open: 1000.0, + close: 1000.0, + prev_close: 998.0, + volume: 1_000_000, + }], + Vec::new(), + vec![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: 324, + ask1_volume: 1_717, + volume_delta: 0, + amount_delta: 0.0, + trading_phase: Some("continuous".to_string()), + }], + ) + .expect("dataset"); + + let subscriptions = BTreeSet::new(); + let portfolio = PortfolioState::new(125_000.0); + let ctx = StrategyContext { + execution_date: date, + decision_date: date, + decision_index: 20, + data: &data, + portfolio: &portfolio, + futures_account: None, + open_orders: &[], + dynamic_universe: None, + subscriptions: &subscriptions, + process_events: &[], + active_process_event: None, + active_datetime: None, + order_events: &[], + fills: &[], + }; + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.aiquant_transaction_cost = true; + cfg.commission_rate = Some(0.0003); + cfg.minimum_commission = Some(5.0); + cfg.strict_value_budget = true; + cfg.slippage_model = SlippageModel::PriceRatio(0.002); + cfg.matching_type = MatchingType::NextTickLast; + cfg.quote_quantity_limit = false; + cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time")); + let strategy = PlatformExprStrategy::new(cfg); + let quote = ctx + .data + .execution_quotes_on(date, symbol) + .first() + .expect("quote"); + + assert_eq!( + strategy.projected_quote_raw_price(quote, OrderSide::Buy), + Some(5.11) + ); + + let mut projected = portfolio.clone(); + let mut execution_state = super::ProjectedExecutionState::default(); + let filled = strategy.project_order_value( + &ctx, + &mut projected, + date, + symbol, + 125_000.0, + &mut execution_state, + ); + + assert_eq!(filled, 24_400); + let position = projected.position(symbol).expect("position"); + assert_eq!(position.quantity, 24_400); + assert!((position.average_cost - 5.12022).abs() < 1e-9); } #[test]