修正平台策略投影撮合价口径

This commit is contained in:
boris
2026-06-16 06:22:40 +08:00
parent 5078aec840
commit d2c65c91b7
+197 -9
View File
@@ -4,7 +4,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime}; use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime};
use rhai::{AST, Dynamic, Engine, Map, Scope}; use rhai::{AST, Dynamic, Engine, Map, Scope};
use crate::broker::SlippageModel; use crate::broker::{MatchingType, SlippageModel};
use crate::cost::ChinaAShareCostModel; use crate::cost::ChinaAShareCostModel;
use crate::data::{ use crate::data::{
DailyMarketSnapshot, EligibleUniverseSnapshot, PriceField, decision_free_float_cap_bn, DailyMarketSnapshot, EligibleUniverseSnapshot, PriceField, decision_free_float_cap_bn,
@@ -215,6 +215,7 @@ pub struct PlatformExprStrategyConfig {
pub stamp_tax_rate_after_change: Option<f64>, pub stamp_tax_rate_after_change: Option<f64>,
pub strict_value_budget: bool, pub strict_value_budget: bool,
pub slippage_model: SlippageModel, pub slippage_model: SlippageModel,
pub matching_type: MatchingType,
pub quote_quantity_limit: bool, pub quote_quantity_limit: bool,
pub current_day_precomputed_factors: bool, pub current_day_precomputed_factors: bool,
pub intraday_execution_time: Option<NaiveTime>, pub intraday_execution_time: Option<NaiveTime>,
@@ -282,6 +283,7 @@ fn band_low(index_close) {
stamp_tax_rate_after_change: None, stamp_tax_rate_after_change: None,
strict_value_budget: false, strict_value_budget: false,
slippage_model: SlippageModel::None, slippage_model: SlippageModel::None,
matching_type: MatchingType::NextTickLast,
quote_quantity_limit: true, quote_quantity_limit: true,
current_day_precomputed_factors: false, current_day_precomputed_factors: false,
intraday_execution_time: None, intraday_execution_time: None,
@@ -1133,7 +1135,7 @@ impl PlatformExprStrategy {
symbol: &str, symbol: &str,
) -> Option<f64> { ) -> Option<f64> {
self.aiquant_scheduled_quote(ctx, date, symbol) 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( fn aiquant_scheduled_last_price(
@@ -1211,6 +1213,41 @@ impl PlatformExprStrategy {
bounded bounded
} }
fn projected_quote_raw_price(
&self,
quote: &crate::data::IntradayExecutionQuote,
side: OrderSide,
) -> Option<f64> {
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( fn projected_execution_limit_rejection_reason(
market: &DailyMarketSnapshot, market: &DailyMarketSnapshot,
side: OrderSide, side: OrderSide,
@@ -1303,10 +1340,7 @@ impl PlatformExprStrategy {
let mut last_timestamp = None; let mut last_timestamp = None;
for quote in selected_quotes { for quote in selected_quotes {
let Some(raw_quote_price) = (match side { let Some(raw_quote_price) = self.projected_quote_raw_price(quote, side) else {
OrderSide::Buy => quote.buy_price(),
OrderSide::Sell => quote.sell_price(),
}) else {
continue; continue;
}; };
let available_qty = if self.config.quote_quantity_limit { let available_qty = if self.config.quote_quantity_limit {
@@ -6975,9 +7009,10 @@ mod tests {
use crate::{ use crate::{
AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction, AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction,
DailyFactorSnapshot, DailyMarketSnapshot, DataSet, FactorTextValue, FuturesCommissionType, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, FactorTextValue, FuturesCommissionType,
FuturesTradingParameter, Instrument, IntradayExecutionQuote, OpenOrderView, OrderIntent, FuturesTradingParameter, Instrument, IntradayExecutionQuote, MatchingType, OpenOrderView,
PortfolioState, ProcessEvent, ProcessEventKind, ScheduleStage, ScheduleTimeRule, Strategy, OrderIntent, OrderSide, PortfolioState, ProcessEvent, ProcessEventKind, ScheduleStage,
StrategyContext, TargetPortfolioOrderPricing, TradingCalendar, default_stage_time, ScheduleTimeRule, SlippageModel, Strategy, StrategyContext, TargetPortfolioOrderPricing,
TradingCalendar, default_stage_time,
}; };
fn d(year: i32, month: u32, day: u32) -> NaiveDate { 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_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_776.0, 11.93, 100, 100), 300);
assert_eq!(strategy.value_buy_quantity(4_848.0, 11.93, 100, 100), 400); 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] #[test]