调整回测撮合为分钟线执行价语义
This commit is contained in:
@@ -73,10 +73,9 @@ pub enum MatchingType {
|
|||||||
OpenAuction,
|
OpenAuction,
|
||||||
CurrentBarClose,
|
CurrentBarClose,
|
||||||
NextBarOpen,
|
NextBarOpen,
|
||||||
NextMinuteLast,
|
MinuteLast,
|
||||||
NextMinuteBestOwn,
|
MinuteBestOwn,
|
||||||
NextMinuteBestCounterparty,
|
MinuteBestCounterparty,
|
||||||
CounterpartyOffer,
|
|
||||||
Vwap,
|
Vwap,
|
||||||
Twap,
|
Twap,
|
||||||
}
|
}
|
||||||
@@ -563,7 +562,7 @@ where
|
|||||||
matching_type: MatchingType,
|
matching_type: MatchingType,
|
||||||
) -> Option<f64> {
|
) -> Option<f64> {
|
||||||
let raw_price = match matching_type {
|
let raw_price = match matching_type {
|
||||||
MatchingType::NextMinuteBestOwn => match side {
|
MatchingType::MinuteBestOwn => match side {
|
||||||
OrderSide::Buy => {
|
OrderSide::Buy => {
|
||||||
if quote.bid1.is_finite() && quote.bid1 > 0.0 {
|
if quote.bid1.is_finite() && quote.bid1 > 0.0 {
|
||||||
Some(quote.bid1)
|
Some(quote.bid1)
|
||||||
@@ -587,13 +586,11 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer => {
|
MatchingType::MinuteBestCounterparty => match side {
|
||||||
match side {
|
|
||||||
OrderSide::Buy => quote.buy_price(),
|
OrderSide::Buy => quote.buy_price(),
|
||||||
OrderSide::Sell => quote.sell_price(),
|
OrderSide::Sell => quote.sell_price(),
|
||||||
}
|
},
|
||||||
}
|
MatchingType::MinuteLast | MatchingType::Vwap | MatchingType::Twap => {
|
||||||
MatchingType::NextMinuteLast | MatchingType::Vwap | MatchingType::Twap => {
|
|
||||||
if quote.last_price.is_finite() && quote.last_price > 0.0 {
|
if quote.last_price.is_finite() && quote.last_price > 0.0 {
|
||||||
Some(quote.last_price)
|
Some(quote.last_price)
|
||||||
} else {
|
} else {
|
||||||
@@ -4998,7 +4995,7 @@ where
|
|||||||
quotes,
|
quotes,
|
||||||
start_cursor,
|
start_cursor,
|
||||||
end_cursor,
|
end_cursor,
|
||||||
matching_type == MatchingType::NextMinuteLast && start_cursor.is_some(),
|
matching_type == MatchingType::MinuteLast && start_cursor.is_some(),
|
||||||
)),
|
)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -5056,12 +5053,12 @@ where
|
|||||||
let quote_quantity_limited =
|
let quote_quantity_limited =
|
||||||
self.quote_quantity_limited_for_window(matching_type, start_cursor, end_cursor);
|
self.quote_quantity_limited_for_window(matching_type, start_cursor, end_cursor);
|
||||||
let lot = round_lot.max(1);
|
let lot = round_lot.max(1);
|
||||||
let exact_time_order_quote = matching_type != MatchingType::NextMinuteLast
|
let exact_time_order_quote = matching_type != MatchingType::MinuteLast
|
||||||
&& start_cursor.is_some()
|
&& start_cursor.is_some()
|
||||||
&& end_cursor.is_some()
|
&& end_cursor.is_some()
|
||||||
&& start_cursor == end_cursor;
|
&& start_cursor == end_cursor;
|
||||||
let use_decision_time_quote = start_cursor.is_some()
|
let use_decision_time_quote = start_cursor.is_some()
|
||||||
&& (matching_type == MatchingType::NextMinuteLast || exact_time_order_quote);
|
&& (matching_type == MatchingType::MinuteLast || exact_time_order_quote);
|
||||||
let eligible_quotes: Vec<&IntradayExecutionQuote> = if use_decision_time_quote {
|
let eligible_quotes: Vec<&IntradayExecutionQuote> = if use_decision_time_quote {
|
||||||
self.latest_known_quote_at_or_before(
|
self.latest_known_quote_at_or_before(
|
||||||
quotes,
|
quotes,
|
||||||
@@ -5290,7 +5287,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn quote_quantity_limited(&self, matching_type: MatchingType) -> bool {
|
fn quote_quantity_limited(&self, matching_type: MatchingType) -> bool {
|
||||||
self.volume_limit || self.liquidity_limit || matching_type != MatchingType::NextMinuteLast
|
self.volume_limit || self.liquidity_limit || matching_type != MatchingType::MinuteLast
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quote_quantity_limited_for_window(
|
fn quote_quantity_limited_for_window(
|
||||||
@@ -5321,7 +5318,7 @@ fn matching_type_from_price_field(field: PriceField) -> MatchingType {
|
|||||||
PriceField::DayOpen => MatchingType::OpenAuction,
|
PriceField::DayOpen => MatchingType::OpenAuction,
|
||||||
PriceField::Open => MatchingType::NextBarOpen,
|
PriceField::Open => MatchingType::NextBarOpen,
|
||||||
PriceField::Close => MatchingType::CurrentBarClose,
|
PriceField::Close => MatchingType::CurrentBarClose,
|
||||||
PriceField::Last => MatchingType::NextMinuteLast,
|
PriceField::Last => MatchingType::MinuteLast,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5487,17 +5484,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn next_minute_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() {
|
fn minute_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() {
|
||||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||||
.with_volume_limit(false)
|
.with_volume_limit(false)
|
||||||
.with_liquidity_limit(false);
|
.with_liquidity_limit(false);
|
||||||
|
|
||||||
assert!(!broker.quote_quantity_limited(MatchingType::NextMinuteLast));
|
assert!(!broker.quote_quantity_limited(MatchingType::MinuteLast));
|
||||||
assert!(broker.quote_quantity_limited(MatchingType::CounterpartyOffer));
|
assert!(broker.quote_quantity_limited(MatchingType::MinuteBestCounterparty));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn next_minute_last_keeps_quote_quantity_cap_when_limits_enabled() {
|
fn minute_last_keeps_quote_quantity_cap_when_limits_enabled() {
|
||||||
let volume_limited =
|
let volume_limited =
|
||||||
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||||
.with_volume_limit(true)
|
.with_volume_limit(true)
|
||||||
@@ -5507,8 +5504,8 @@ mod tests {
|
|||||||
.with_volume_limit(false)
|
.with_volume_limit(false)
|
||||||
.with_liquidity_limit(true);
|
.with_liquidity_limit(true);
|
||||||
|
|
||||||
assert!(volume_limited.quote_quantity_limited(MatchingType::NextMinuteLast));
|
assert!(volume_limited.quote_quantity_limited(MatchingType::MinuteLast));
|
||||||
assert!(liquidity_limited.quote_quantity_limited(MatchingType::NextMinuteLast));
|
assert!(liquidity_limited.quote_quantity_limited(MatchingType::MinuteLast));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -5558,7 +5555,7 @@ mod tests {
|
|||||||
PriceField::Last,
|
PriceField::Last,
|
||||||
)
|
)
|
||||||
.with_intraday_execution_start_time(date.and_hms_opt(10, 18, 0).unwrap().time())
|
.with_intraday_execution_start_time(date.and_hms_opt(10, 18, 0).unwrap().time())
|
||||||
.with_matching_type(MatchingType::NextMinuteLast)
|
.with_matching_type(MatchingType::MinuteLast)
|
||||||
.with_slippage_model(SlippageModel::PriceRatio(0.001))
|
.with_slippage_model(SlippageModel::PriceRatio(0.001))
|
||||||
.with_volume_limit(false)
|
.with_volume_limit(false)
|
||||||
.with_liquidity_limit(false);
|
.with_liquidity_limit(false);
|
||||||
@@ -5751,7 +5748,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn next_minute_last_execution_uses_latest_quote_before_decision_time() {
|
fn minute_last_execution_uses_latest_quote_before_decision_time() {
|
||||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||||
let broker = BrokerSimulator::new_with_execution_price(
|
let broker = BrokerSimulator::new_with_execution_price(
|
||||||
ChinaAShareCostModel::default(),
|
ChinaAShareCostModel::default(),
|
||||||
@@ -5772,7 +5769,7 @@ mod tests {
|
|||||||
&snapshot,
|
&snapshot,
|
||||||
&[quote],
|
&[quote],
|
||||||
OrderSide::Sell,
|
OrderSide::Sell,
|
||||||
MatchingType::NextMinuteLast,
|
MatchingType::MinuteLast,
|
||||||
Some(decision_time),
|
Some(decision_time),
|
||||||
Some(decision_time),
|
Some(decision_time),
|
||||||
200,
|
200,
|
||||||
@@ -6026,7 +6023,7 @@ mod tests {
|
|||||||
&snapshot,
|
&snapshot,
|
||||||
&[quote],
|
&[quote],
|
||||||
OrderSide::Buy,
|
OrderSide::Buy,
|
||||||
MatchingType::NextMinuteLast,
|
MatchingType::MinuteLast,
|
||||||
Some(start),
|
Some(start),
|
||||||
None,
|
None,
|
||||||
100,
|
100,
|
||||||
@@ -6103,7 +6100,7 @@ mod tests {
|
|||||||
&snapshot,
|
&snapshot,
|
||||||
&[quote],
|
&[quote],
|
||||||
OrderSide::Sell,
|
OrderSide::Sell,
|
||||||
MatchingType::NextMinuteLast,
|
MatchingType::MinuteLast,
|
||||||
Some(start),
|
Some(start),
|
||||||
None,
|
None,
|
||||||
100,
|
100,
|
||||||
|
|||||||
@@ -1444,7 +1444,7 @@ where
|
|||||||
let snapshot = self.data.market(date, &intent.symbol);
|
let snapshot = self.data.market(date, &intent.symbol);
|
||||||
if matches!(
|
if matches!(
|
||||||
self.broker.matching_type(),
|
self.broker.matching_type(),
|
||||||
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer
|
MatchingType::MinuteBestCounterparty
|
||||||
) {
|
) {
|
||||||
let depth = self.data.order_book_depth_on(date, &intent.symbol);
|
let depth = self.data.order_book_depth_on(date, &intent.symbol);
|
||||||
if !depth.is_empty() {
|
if !depth.is_empty() {
|
||||||
@@ -1454,7 +1454,7 @@ where
|
|||||||
let quotes = self.data.execution_quotes_on(date, &intent.symbol);
|
let quotes = self.data.execution_quotes_on(date, &intent.symbol);
|
||||||
for quote in quotes {
|
for quote in quotes {
|
||||||
let price = match self.broker.matching_type() {
|
let price = match self.broker.matching_type() {
|
||||||
MatchingType::NextMinuteBestOwn => match intent.side() {
|
MatchingType::MinuteBestOwn => match intent.side() {
|
||||||
OrderSide::Buy => {
|
OrderSide::Buy => {
|
||||||
if quote.bid1.is_finite() && quote.bid1 > 0.0 {
|
if quote.bid1.is_finite() && quote.bid1 > 0.0 {
|
||||||
quote.bid1
|
quote.bid1
|
||||||
@@ -1470,12 +1470,10 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer => {
|
MatchingType::MinuteBestCounterparty => match intent.side() {
|
||||||
match intent.side() {
|
|
||||||
OrderSide::Buy => quote.buy_price().unwrap_or(quote.last_price),
|
OrderSide::Buy => quote.buy_price().unwrap_or(quote.last_price),
|
||||||
OrderSide::Sell => quote.sell_price().unwrap_or(quote.last_price),
|
OrderSide::Sell => quote.sell_price().unwrap_or(quote.last_price),
|
||||||
}
|
},
|
||||||
}
|
|
||||||
_ => quote.last_price,
|
_ => quote.last_price,
|
||||||
};
|
};
|
||||||
if let Some(snapshot) = snapshot {
|
if let Some(snapshot) = snapshot {
|
||||||
|
|||||||
@@ -286,7 +286,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::NextMinuteLast,
|
matching_type: MatchingType::MinuteLast,
|
||||||
quote_quantity_limit: true,
|
quote_quantity_limit: true,
|
||||||
current_day_precomputed_factors: false,
|
current_day_precomputed_factors: false,
|
||||||
prefer_precomputed_rolling_factors: false,
|
prefer_precomputed_rolling_factors: false,
|
||||||
@@ -1342,7 +1342,7 @@ impl PlatformExprStrategy {
|
|||||||
let last =
|
let last =
|
||||||
|| (quote.last_price.is_finite() && quote.last_price > 0.0).then_some(quote.last_price);
|
|| (quote.last_price.is_finite() && quote.last_price > 0.0).then_some(quote.last_price);
|
||||||
match self.config.matching_type {
|
match self.config.matching_type {
|
||||||
MatchingType::NextMinuteBestOwn => match side {
|
MatchingType::MinuteBestOwn => match side {
|
||||||
OrderSide::Buy => (quote.bid1.is_finite() && quote.bid1 > 0.0)
|
OrderSide::Buy => (quote.bid1.is_finite() && quote.bid1 > 0.0)
|
||||||
.then_some(quote.bid1)
|
.then_some(quote.bid1)
|
||||||
.or_else(last),
|
.or_else(last),
|
||||||
@@ -1350,17 +1350,16 @@ impl PlatformExprStrategy {
|
|||||||
.then_some(quote.ask1)
|
.then_some(quote.ask1)
|
||||||
.or_else(last),
|
.or_else(last),
|
||||||
},
|
},
|
||||||
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer => {
|
MatchingType::MinuteBestCounterparty => match side {
|
||||||
match side {
|
|
||||||
OrderSide::Buy => quote.buy_price(),
|
OrderSide::Buy => quote.buy_price(),
|
||||||
OrderSide::Sell => quote.sell_price(),
|
OrderSide::Sell => quote.sell_price(),
|
||||||
}
|
},
|
||||||
}
|
MatchingType::MinuteLast | MatchingType::Vwap | MatchingType::Twap => {
|
||||||
MatchingType::NextMinuteLast | MatchingType::Vwap | MatchingType::Twap => last()
|
last().or_else(|| match side {
|
||||||
.or_else(|| match side {
|
|
||||||
OrderSide::Buy => quote.buy_price(),
|
OrderSide::Buy => quote.buy_price(),
|
||||||
OrderSide::Sell => quote.sell_price(),
|
OrderSide::Sell => quote.sell_price(),
|
||||||
}),
|
})
|
||||||
|
}
|
||||||
_ => match side {
|
_ => match side {
|
||||||
OrderSide::Buy => quote.buy_price(),
|
OrderSide::Buy => quote.buy_price(),
|
||||||
OrderSide::Sell => quote.sell_price(),
|
OrderSide::Sell => quote.sell_price(),
|
||||||
@@ -8423,7 +8422,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn platform_aiquant_next_minute_last_projection_uses_last_quote_for_buy_budget() {
|
fn platform_aiquant_minute_last_projection_uses_last_quote_for_buy_budget() {
|
||||||
let date = d(2023, 5, 4);
|
let date = d(2023, 5, 4);
|
||||||
let symbol = "000782.SZ";
|
let symbol = "000782.SZ";
|
||||||
let data = DataSet::from_components_with_actions_and_quotes(
|
let data = DataSet::from_components_with_actions_and_quotes(
|
||||||
@@ -8530,7 +8529,7 @@ mod tests {
|
|||||||
cfg.minimum_commission = Some(5.0);
|
cfg.minimum_commission = Some(5.0);
|
||||||
cfg.strict_value_budget = true;
|
cfg.strict_value_budget = true;
|
||||||
cfg.slippage_model = SlippageModel::PriceRatio(0.002);
|
cfg.slippage_model = SlippageModel::PriceRatio(0.002);
|
||||||
cfg.matching_type = MatchingType::NextMinuteLast;
|
cfg.matching_type = MatchingType::MinuteLast;
|
||||||
cfg.quote_quantity_limit = false;
|
cfg.quote_quantity_limit = false;
|
||||||
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time"));
|
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time"));
|
||||||
let strategy = PlatformExprStrategy::new(cfg);
|
let strategy = PlatformExprStrategy::new(cfg);
|
||||||
@@ -8688,7 +8687,7 @@ mod tests {
|
|||||||
cfg.minimum_commission = Some(5.0);
|
cfg.minimum_commission = Some(5.0);
|
||||||
cfg.strict_value_budget = true;
|
cfg.strict_value_budget = true;
|
||||||
cfg.slippage_model = SlippageModel::PriceRatio(0.002);
|
cfg.slippage_model = SlippageModel::PriceRatio(0.002);
|
||||||
cfg.matching_type = MatchingType::NextMinuteLast;
|
cfg.matching_type = MatchingType::MinuteLast;
|
||||||
cfg.quote_quantity_limit = false;
|
cfg.quote_quantity_limit = false;
|
||||||
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time"));
|
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time"));
|
||||||
let strategy = PlatformExprStrategy::new(cfg);
|
let strategy = PlatformExprStrategy::new(cfg);
|
||||||
|
|||||||
@@ -423,10 +423,9 @@ fn parse_matching_type(value: Option<&str>) -> Option<MatchingType> {
|
|||||||
"open_auction" => Some(MatchingType::OpenAuction),
|
"open_auction" => Some(MatchingType::OpenAuction),
|
||||||
"current_bar_close" => Some(MatchingType::CurrentBarClose),
|
"current_bar_close" => Some(MatchingType::CurrentBarClose),
|
||||||
"next_bar_open" => Some(MatchingType::NextBarOpen),
|
"next_bar_open" => Some(MatchingType::NextBarOpen),
|
||||||
"next_minute_last" => Some(MatchingType::NextMinuteLast),
|
"minute_last" => Some(MatchingType::MinuteLast),
|
||||||
"next_minute_best_own" => Some(MatchingType::NextMinuteBestOwn),
|
"minute_best_own" => Some(MatchingType::MinuteBestOwn),
|
||||||
"next_minute_best_counterparty" => Some(MatchingType::NextMinuteBestCounterparty),
|
"minute_best_counterparty" => Some(MatchingType::MinuteBestCounterparty),
|
||||||
"counterparty_offer" => Some(MatchingType::CounterpartyOffer),
|
|
||||||
"vwap" => Some(MatchingType::Vwap),
|
"vwap" => Some(MatchingType::Vwap),
|
||||||
"twap" => Some(MatchingType::Twap),
|
"twap" => Some(MatchingType::Twap),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -1655,7 +1654,7 @@ mod tests {
|
|||||||
fn parses_execution_slippage_overrides_into_platform_config() {
|
fn parses_execution_slippage_overrides_into_platform_config() {
|
||||||
let spec = serde_json::json!({
|
let spec = serde_json::json!({
|
||||||
"execution": {
|
"execution": {
|
||||||
"matchingType": "next_minute_last",
|
"matchingType": "minute_last",
|
||||||
"slippageModel": "price_ratio",
|
"slippageModel": "price_ratio",
|
||||||
"slippageValue": 0.001,
|
"slippageValue": 0.001,
|
||||||
"strictValueBudget": true
|
"strictValueBudget": true
|
||||||
@@ -1671,7 +1670,7 @@ mod tests {
|
|||||||
|
|
||||||
let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
|
let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
|
||||||
|
|
||||||
assert_eq!(cfg.matching_type, MatchingType::NextMinuteLast);
|
assert_eq!(cfg.matching_type, MatchingType::MinuteLast);
|
||||||
assert_eq!(cfg.slippage_model, SlippageModel::PriceRatio(0.001));
|
assert_eq!(cfg.slippage_model, SlippageModel::PriceRatio(0.001));
|
||||||
assert!(cfg.strict_value_budget);
|
assert!(cfg.strict_value_budget);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
},
|
},
|
||||||
ManualSection {
|
ManualSection {
|
||||||
title: "execution.matching_type / execution.slippage".to_string(),
|
title: "execution.matching_type / execution.slippage".to_string(),
|
||||||
detail: "设置撮合模式和滑点。使用 execution.matching_type(\"next_minute_last\" | \"next_minute_best_own\" | \"next_minute_best_counterparty\" | \"counterparty_offer\" | \"vwap\" | \"current_bar_close\" | \"next_bar_open\" | \"open_auction\")。next_minute_last 使用分钟执行价 last_price;next_minute_best_own / next_minute_best_counterparty 会按 L1 买一卖一近似 平台内核 的最优价语义;counterparty_offer 在存在 order_book_depth 多档盘口数据时会按真实档位逐档扫单并计算加权成交价,不存在 depth 时回退 L1 对手方报价;vwap 会在盘中执行价链路上聚合多笔成交为单条 VWAP 成交;next_bar_open 使用决策日信号并在下一可交易日开盘撮合,禁止把执行日 open/high/low/close 解释为下单前已知数据;open_auction 使用当日集合竞价开盘价 day_open 进行撮合,且不额外施加滑点,并按竞价成交量而不是盘口一档流动性限制成交;滑点支持 execution.slippage(\"none\") / execution.slippage(\"price_ratio\", 0.001) / execution.slippage(\"tick_size\", 1) / execution.slippage(\"limit_price\"),其中 limit_price 会在限价单成交时按挂单价模拟 平台内核 的最坏成交价。".to_string(),
|
detail: "设置撮合模式和滑点。使用 execution.matching_type(\"minute_last\" | \"minute_best_own\" | \"minute_best_counterparty\" | \"vwap\" | \"twap\" | \"current_bar_close\" | \"next_bar_open\" | \"open_auction\")。minute_last 使用分钟执行价 last_price;minute_best_own 使用己方一档报价,minute_best_counterparty 使用对手方一档报价,存在 order_book_depth 多档盘口数据时可按真实档位逐档扫单;vwap/twap 在分钟执行价窗口内聚合成交;next_bar_open 使用决策日信号并在下一可交易日开盘撮合,禁止把执行日 open/high/low/close 解释为下单前已知数据;open_auction 使用当日集合竞价开盘价 day_open 进行撮合,且不额外施加滑点,并按竞价成交量而不是盘口一档流动性限制成交;滑点支持 execution.slippage(\"none\") / execution.slippage(\"price_ratio\", 0.001) / execution.slippage(\"tick_size\", 1) / execution.slippage(\"limit_price\"),其中 limit_price 会在限价单成交时按挂单价模拟 平台内核 的最坏成交价。".to_string(),
|
||||||
},
|
},
|
||||||
ManualSection {
|
ManualSection {
|
||||||
title: "期货提交校验".to_string(),
|
title: "期货提交校验".to_string(),
|
||||||
@@ -360,7 +360,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
},
|
},
|
||||||
ManualFactorSource {
|
ManualFactorSource {
|
||||||
table: "盘口深度参数".to_string(),
|
table: "盘口深度参数".to_string(),
|
||||||
detail: "可选字段包括 date、symbol、timestamp、level、bid_price、bid_volume、ask_price、ask_volume。存在盘口深度时,期货 counterparty_offer / next_minute_best_counterparty 可按真实多档盘口逐档扫单;不存在时不会伪造 depth。".to_string(),
|
detail: "可选字段包括 date、symbol、timestamp、level、bid_price、bid_volume、ask_price、ask_volume。存在盘口深度时,期货 minute_best_counterparty 可按真实多档盘口逐档扫单;不存在时不会伪造 depth。".to_string(),
|
||||||
fields: vec![],
|
fields: vec![],
|
||||||
},
|
},
|
||||||
ManualFactorSource {
|
ManualFactorSource {
|
||||||
@@ -384,7 +384,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
},
|
},
|
||||||
ManualExample {
|
ManualExample {
|
||||||
title: "分钟执行价撮合 + 最小价位滑点".to_string(),
|
title: "分钟执行价撮合 + 最小价位滑点".to_string(),
|
||||||
code: "execution.matching_type(\"next_minute_last\")\nexecution.slippage(\"tick_size\", 1)".to_string(),
|
code: "execution.matching_type(\"minute_last\")\nexecution.slippage(\"tick_size\", 1)".to_string(),
|
||||||
},
|
},
|
||||||
ManualExample {
|
ManualExample {
|
||||||
title: "动态 universe 和订阅".to_string(),
|
title: "动态 universe 和订阅".to_string(),
|
||||||
@@ -518,7 +518,7 @@ pub fn build_generation_prompt(
|
|||||||
prompt.push_str("- 生成的代码必须能转换为 strategy_spec 并提交 POST /v1/backtests。\n");
|
prompt.push_str("- 生成的代码必须能转换为 strategy_spec 并提交 POST /v1/backtests。\n");
|
||||||
prompt.push_str("- 不要使用手册未列出的字段、函数或外部平台 API 名称。\n\n");
|
prompt.push_str("- 不要使用手册未列出的字段、函数或外部平台 API 名称。\n\n");
|
||||||
prompt.push_str("只允许使用这些可编译语句:market、benchmark、signal、rebalance.every_days(...).at([...])、selection.limit、selection.market_cap_band、filter.stock_ma、filter.stock_expr、ordering.rank_by、ordering.rank_expr、allocation.buy_scale、risk.stop_loss、risk.take_profit、risk.index_exposure、execution.matching_type、execution.slippage、universe.exclude。禁止输出 filter(...)、rank(...)、select.top(...)、weight.equal()、sell_rule(...)、backtest(...)、risk.max_position(...) 这类未支持伪语法。\n");
|
prompt.push_str("只允许使用这些可编译语句:market、benchmark、signal、rebalance.every_days(...).at([...])、selection.limit、selection.market_cap_band、filter.stock_ma、filter.stock_expr、ordering.rank_by、ordering.rank_expr、allocation.buy_scale、risk.stop_loss、risk.take_profit、risk.index_exposure、execution.matching_type、execution.slippage、universe.exclude。禁止输出 filter(...)、rank(...)、select.top(...)、weight.equal()、sell_rule(...)、backtest(...)、risk.max_position(...) 这类未支持伪语法。\n");
|
||||||
prompt.push_str("参数形态必须严格:selection.market_cap_band 必须写 field=\"market_cap\" 或 field=\"free_float_cap\", lower=..., upper=...;禁止使用 float_market_cap;禁止使用 ma60、stock_ma60、signal_ma60、benchmark_ma60,60日价格均线写 rolling_mean(\"close\", 60) 或 ma(\"close\", 60),任意窗口均量写 rolling_mean(\"volume\", n) 或 vma(n);不要生成 fn score() 这类零参数函数,股票字段排序直接写在 ordering.rank_expr 内或用带参数函数;布尔字段按布尔使用,写 !is_st、!paused、!at_upper_limit、!at_lower_limit,不要写 is_st == 0;risk.index_exposure 只能传一个数值表达式,不要使用 risk.exposure;完整三元表达式 cond ? a : b 可以使用,但不得输出残缺问号/冒号片段;execution.matching_type 只能取 next_minute_last、next_minute_best_own、next_minute_best_counterparty、counterparty_offer、vwap、current_bar_close、next_bar_open、open_auction;next_bar_open 只能使用决策日信号,不能把执行日价格当作下单前信息;execution.slippage 必须写 execution.slippage(\"none\") 或 execution.slippage(\"price_ratio\", 0.001)。\n");
|
prompt.push_str("参数形态必须严格:selection.market_cap_band 必须写 field=\"market_cap\" 或 field=\"free_float_cap\", lower=..., upper=...;禁止使用 float_market_cap;禁止使用 ma60、stock_ma60、signal_ma60、benchmark_ma60,60日价格均线写 rolling_mean(\"close\", 60) 或 ma(\"close\", 60),任意窗口均量写 rolling_mean(\"volume\", n) 或 vma(n);不要生成 fn score() 这类零参数函数,股票字段排序直接写在 ordering.rank_expr 内或用带参数函数;布尔字段按布尔使用,写 !is_st、!paused、!at_upper_limit、!at_lower_limit,不要写 is_st == 0;risk.index_exposure 只能传一个数值表达式,不要使用 risk.exposure;完整三元表达式 cond ? a : b 可以使用,但不得输出残缺问号/冒号片段;execution.matching_type 只能取 minute_last、minute_best_own、minute_best_counterparty、vwap、twap、current_bar_close、next_bar_open、open_auction;next_bar_open 只能使用决策日信号,不能把执行日价格当作下单前信息;execution.slippage 必须写 execution.slippage(\"none\") 或 execution.slippage(\"price_ratio\", 0.001)。\n");
|
||||||
prompt.push_str("回测成功但 tradeCount=0 或 holdingCount=0 是无效策略;第一版必须保持稳定买入覆盖率,复杂因子只能在后续优化中逐步加严。\n");
|
prompt.push_str("回测成功但 tradeCount=0 或 holdingCount=0 是无效策略;第一版必须保持稳定买入覆盖率,复杂因子只能在后续优化中逐步加严。\n");
|
||||||
prompt.push_str("可参考但不要照抄的最小模板,回复时不要包含 ``` 代码围栏:\nstrategy(\"cn_a_smallcap_factor_rotation\") {\nmarket(\"CN_A\")\nbenchmark(\"000852.SH\")\nsignal(\"000001.SH\")\nrebalance.every_days(5).at([\"10:18\"])\nselection.limit(40)\nselection.market_cap_band(field=\"market_cap\", lower=0, upper=1000)\nfilter.stock_expr(listed_days >= 60 && !is_st && !paused && close > 2 && !at_upper_limit && !at_lower_limit)\nordering.rank_by(\"market_cap\", \"asc\")\nallocation.buy_scale(1.0)\nrisk.index_exposure(1.0)\nrisk.stop_loss(holding_return < -0.08)\nexecution.slippage(\"price_ratio\", 0.001)\n}\n\n");
|
prompt.push_str("可参考但不要照抄的最小模板,回复时不要包含 ``` 代码围栏:\nstrategy(\"cn_a_smallcap_factor_rotation\") {\nmarket(\"CN_A\")\nbenchmark(\"000852.SH\")\nsignal(\"000001.SH\")\nrebalance.every_days(5).at([\"10:18\"])\nselection.limit(40)\nselection.market_cap_band(field=\"market_cap\", lower=0, upper=1000)\nfilter.stock_expr(listed_days >= 60 && !is_st && !paused && close > 2 && !at_upper_limit && !at_lower_limit)\nordering.rank_by(\"market_cap\", \"asc\")\nallocation.buy_scale(1.0)\nrisk.index_exposure(1.0)\nrisk.stop_loss(holding_return < -0.08)\nexecution.slippage(\"price_ratio\", 0.001)\n}\n\n");
|
||||||
prompt.push_str("用户目标:\n");
|
prompt.push_str("用户目标:\n");
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
|||||||
ChinaEquityRuleHooks,
|
ChinaEquityRuleHooks,
|
||||||
PriceField::Last,
|
PriceField::Last,
|
||||||
)
|
)
|
||||||
.with_matching_type(MatchingType::NextMinuteLast)
|
.with_matching_type(MatchingType::MinuteLast)
|
||||||
.with_intraday_execution_start_time(t(10, 40, 0));
|
.with_intraday_execution_start_time(t(10, 40, 0));
|
||||||
let config = BacktestConfig {
|
let config = BacktestConfig {
|
||||||
initial_cash: 10_000.0,
|
initial_cash: 10_000.0,
|
||||||
@@ -385,7 +385,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
ChinaEquityRuleHooks,
|
ChinaEquityRuleHooks,
|
||||||
PriceField::Last,
|
PriceField::Last,
|
||||||
)
|
)
|
||||||
.with_matching_type(MatchingType::NextMinuteLast)
|
.with_matching_type(MatchingType::MinuteLast)
|
||||||
.with_intraday_execution_start_time(t(10, 40, 0));
|
.with_intraday_execution_start_time(t(10, 40, 0));
|
||||||
let config = BacktestConfig {
|
let config = BacktestConfig {
|
||||||
initial_cash: 10_000.0,
|
initial_cash: 10_000.0,
|
||||||
@@ -587,7 +587,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
|||||||
ChinaEquityRuleHooks,
|
ChinaEquityRuleHooks,
|
||||||
PriceField::Last,
|
PriceField::Last,
|
||||||
)
|
)
|
||||||
.with_matching_type(MatchingType::NextMinuteLast)
|
.with_matching_type(MatchingType::MinuteLast)
|
||||||
.with_intraday_execution_start_time(t(10, 40, 0));
|
.with_intraday_execution_start_time(t(10, 40, 0));
|
||||||
let config = BacktestConfig {
|
let config = BacktestConfig {
|
||||||
initial_cash: 10_000.0,
|
initial_cash: 10_000.0,
|
||||||
|
|||||||
@@ -1842,7 +1842,7 @@ fn engine_sweeps_futures_order_book_depth_when_available() {
|
|||||||
ChinaEquityRuleHooks::default(),
|
ChinaEquityRuleHooks::default(),
|
||||||
PriceField::Last,
|
PriceField::Last,
|
||||||
)
|
)
|
||||||
.with_matching_type(MatchingType::CounterpartyOffer);
|
.with_matching_type(MatchingType::MinuteBestCounterparty);
|
||||||
let mut engine = BacktestEngine::new(
|
let mut engine = BacktestEngine::new(
|
||||||
data,
|
data,
|
||||||
FuturesDepthLimitOrderStrategy,
|
FuturesDepthLimitOrderStrategy,
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ fn broker_delayed_limit_open_sell_uses_minute_price() {
|
|||||||
ChinaEquityRuleHooks::default(),
|
ChinaEquityRuleHooks::default(),
|
||||||
PriceField::Last,
|
PriceField::Last,
|
||||||
)
|
)
|
||||||
.with_matching_type(MatchingType::NextMinuteLast)
|
.with_matching_type(MatchingType::MinuteLast)
|
||||||
.with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 31, 0).unwrap())
|
.with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 31, 0).unwrap())
|
||||||
.with_volume_limit(false)
|
.with_volume_limit(false)
|
||||||
.with_liquidity_limit(false);
|
.with_liquidity_limit(false);
|
||||||
@@ -2888,7 +2888,7 @@ fn broker_uses_best_own_price_for_intraday_matching() {
|
|||||||
ChinaEquityRuleHooks::default(),
|
ChinaEquityRuleHooks::default(),
|
||||||
PriceField::Last,
|
PriceField::Last,
|
||||||
)
|
)
|
||||||
.with_matching_type(MatchingType::NextMinuteBestOwn);
|
.with_matching_type(MatchingType::MinuteBestOwn);
|
||||||
|
|
||||||
let report = broker
|
let report = broker
|
||||||
.execute(
|
.execute(
|
||||||
@@ -3003,7 +3003,7 @@ fn broker_uses_best_counterparty_price_for_intraday_matching() {
|
|||||||
ChinaEquityRuleHooks::default(),
|
ChinaEquityRuleHooks::default(),
|
||||||
PriceField::Last,
|
PriceField::Last,
|
||||||
)
|
)
|
||||||
.with_matching_type(MatchingType::NextMinuteBestCounterparty);
|
.with_matching_type(MatchingType::MinuteBestCounterparty);
|
||||||
|
|
||||||
let report = broker
|
let report = broker
|
||||||
.execute(
|
.execute(
|
||||||
|
|||||||
Reference in New Issue
Block a user