切换回测执行价为分钟线语义
This commit is contained in:
@@ -73,9 +73,9 @@ pub enum MatchingType {
|
||||
OpenAuction,
|
||||
CurrentBarClose,
|
||||
NextBarOpen,
|
||||
NextTickLast,
|
||||
NextTickBestOwn,
|
||||
NextTickBestCounterparty,
|
||||
NextMinuteLast,
|
||||
NextMinuteBestOwn,
|
||||
NextMinuteBestCounterparty,
|
||||
CounterpartyOffer,
|
||||
Vwap,
|
||||
Twap,
|
||||
@@ -563,7 +563,7 @@ where
|
||||
matching_type: MatchingType,
|
||||
) -> Option<f64> {
|
||||
let raw_price = match matching_type {
|
||||
MatchingType::NextTickBestOwn => match side {
|
||||
MatchingType::NextMinuteBestOwn => match side {
|
||||
OrderSide::Buy => {
|
||||
if quote.bid1.is_finite() && quote.bid1 > 0.0 {
|
||||
Some(quote.bid1)
|
||||
@@ -587,13 +587,13 @@ where
|
||||
}
|
||||
}
|
||||
},
|
||||
MatchingType::NextTickBestCounterparty | MatchingType::CounterpartyOffer => {
|
||||
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer => {
|
||||
match side {
|
||||
OrderSide::Buy => quote.buy_price(),
|
||||
OrderSide::Sell => quote.sell_price(),
|
||||
}
|
||||
}
|
||||
MatchingType::NextTickLast | MatchingType::Vwap | MatchingType::Twap => {
|
||||
MatchingType::NextMinuteLast | MatchingType::Vwap | MatchingType::Twap => {
|
||||
if quote.last_price.is_finite() && quote.last_price > 0.0 {
|
||||
Some(quote.last_price)
|
||||
} else {
|
||||
@@ -4806,7 +4806,7 @@ where
|
||||
}
|
||||
|
||||
if self.inactive_limit && snapshot.tick_volume == 0 {
|
||||
return Err("tick no volume".to_string());
|
||||
return Err("minute no volume".to_string());
|
||||
}
|
||||
|
||||
let mut max_fill = requested_qty;
|
||||
@@ -4835,7 +4835,7 @@ where
|
||||
let raw_limit = ((snapshot.tick_volume as f64) * self.volume_percent).round() as i64
|
||||
- consumed_turnover as i64;
|
||||
if raw_limit <= 0 {
|
||||
return Err("tick volume limit".to_string());
|
||||
return Err("minute volume limit".to_string());
|
||||
}
|
||||
let volume_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
|
||||
raw_limit as u32
|
||||
@@ -4843,7 +4843,7 @@ where
|
||||
self.round_buy_quantity(raw_limit as u32, minimum_order_quantity, order_step_size)
|
||||
};
|
||||
if volume_limited == 0 {
|
||||
return Err("tick volume limit".to_string());
|
||||
return Err("minute volume limit".to_string());
|
||||
}
|
||||
max_fill = max_fill.min(volume_limited);
|
||||
}
|
||||
@@ -4998,7 +4998,7 @@ where
|
||||
quotes,
|
||||
start_cursor,
|
||||
end_cursor,
|
||||
matching_type == MatchingType::NextTickLast && start_cursor.is_some(),
|
||||
matching_type == MatchingType::NextMinuteLast && start_cursor.is_some(),
|
||||
)),
|
||||
});
|
||||
}
|
||||
@@ -5056,12 +5056,12 @@ where
|
||||
let quote_quantity_limited =
|
||||
self.quote_quantity_limited_for_window(matching_type, start_cursor, end_cursor);
|
||||
let lot = round_lot.max(1);
|
||||
let exact_time_order_quote = matching_type != MatchingType::NextTickLast
|
||||
let exact_time_order_quote = matching_type != MatchingType::NextMinuteLast
|
||||
&& start_cursor.is_some()
|
||||
&& end_cursor.is_some()
|
||||
&& start_cursor == end_cursor;
|
||||
let use_decision_time_quote = start_cursor.is_some()
|
||||
&& (matching_type == MatchingType::NextTickLast || exact_time_order_quote);
|
||||
&& (matching_type == MatchingType::NextMinuteLast || exact_time_order_quote);
|
||||
let eligible_quotes: Vec<&IntradayExecutionQuote> = if use_decision_time_quote {
|
||||
self.latest_known_quote_at_or_before(
|
||||
quotes,
|
||||
@@ -5290,7 +5290,7 @@ where
|
||||
}
|
||||
|
||||
fn quote_quantity_limited(&self, matching_type: MatchingType) -> bool {
|
||||
self.volume_limit || self.liquidity_limit || matching_type != MatchingType::NextTickLast
|
||||
self.volume_limit || self.liquidity_limit || matching_type != MatchingType::NextMinuteLast
|
||||
}
|
||||
|
||||
fn quote_quantity_limited_for_window(
|
||||
@@ -5321,7 +5321,7 @@ fn matching_type_from_price_field(field: PriceField) -> MatchingType {
|
||||
PriceField::DayOpen => MatchingType::OpenAuction,
|
||||
PriceField::Open => MatchingType::NextBarOpen,
|
||||
PriceField::Close => MatchingType::CurrentBarClose,
|
||||
PriceField::Last => MatchingType::NextTickLast,
|
||||
PriceField::Last => MatchingType::NextMinuteLast,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5338,8 +5338,8 @@ fn merge_partial_fill_reason(current: Option<String>, next: Option<&str>) -> Opt
|
||||
|
||||
fn zero_fill_status_for_reason(reason: &str) -> OrderStatus {
|
||||
match reason {
|
||||
"tick no volume"
|
||||
| "tick volume limit"
|
||||
"minute no volume"
|
||||
| "minute volume limit"
|
||||
| "intraday quote liquidity exhausted"
|
||||
| "no execution quotes at or before start"
|
||||
| "no execution quotes after start"
|
||||
@@ -5487,17 +5487,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_tick_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() {
|
||||
fn next_minute_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() {
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false);
|
||||
|
||||
assert!(!broker.quote_quantity_limited(MatchingType::NextTickLast));
|
||||
assert!(!broker.quote_quantity_limited(MatchingType::NextMinuteLast));
|
||||
assert!(broker.quote_quantity_limited(MatchingType::CounterpartyOffer));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_tick_last_keeps_quote_quantity_cap_when_limits_enabled() {
|
||||
fn next_minute_last_keeps_quote_quantity_cap_when_limits_enabled() {
|
||||
let volume_limited =
|
||||
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_volume_limit(true)
|
||||
@@ -5507,8 +5507,8 @@ mod tests {
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(true);
|
||||
|
||||
assert!(volume_limited.quote_quantity_limited(MatchingType::NextTickLast));
|
||||
assert!(liquidity_limited.quote_quantity_limited(MatchingType::NextTickLast));
|
||||
assert!(volume_limited.quote_quantity_limited(MatchingType::NextMinuteLast));
|
||||
assert!(liquidity_limited.quote_quantity_limited(MatchingType::NextMinuteLast));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5558,7 +5558,7 @@ mod tests {
|
||||
PriceField::Last,
|
||||
)
|
||||
.with_intraday_execution_start_time(date.and_hms_opt(10, 18, 0).unwrap().time())
|
||||
.with_matching_type(MatchingType::NextTickLast)
|
||||
.with_matching_type(MatchingType::NextMinuteLast)
|
||||
.with_slippage_model(SlippageModel::PriceRatio(0.001))
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false);
|
||||
@@ -5751,7 +5751,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_tick_last_execution_uses_latest_quote_before_decision_time() {
|
||||
fn next_minute_last_execution_uses_latest_quote_before_decision_time() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
@@ -5772,7 +5772,7 @@ mod tests {
|
||||
&snapshot,
|
||||
&[quote],
|
||||
OrderSide::Sell,
|
||||
MatchingType::NextTickLast,
|
||||
MatchingType::NextMinuteLast,
|
||||
Some(decision_time),
|
||||
Some(decision_time),
|
||||
200,
|
||||
@@ -6026,7 +6026,7 @@ mod tests {
|
||||
&snapshot,
|
||||
&[quote],
|
||||
OrderSide::Buy,
|
||||
MatchingType::NextTickLast,
|
||||
MatchingType::NextMinuteLast,
|
||||
Some(start),
|
||||
None,
|
||||
100,
|
||||
@@ -6103,7 +6103,7 @@ mod tests {
|
||||
&snapshot,
|
||||
&[quote],
|
||||
OrderSide::Sell,
|
||||
MatchingType::NextTickLast,
|
||||
MatchingType::NextMinuteLast,
|
||||
Some(start),
|
||||
None,
|
||||
100,
|
||||
|
||||
@@ -3090,7 +3090,7 @@ fn normalize_history_frequency(frequency: &str) -> Option<String> {
|
||||
let normalized = normalize_field(frequency);
|
||||
match normalized.as_str() {
|
||||
"1d" | "d" | "day" | "daily" => Some("1d".to_string()),
|
||||
"1m" | "m" | "minute" | "min" | "tick" | "t" => Some("1m".to_string()),
|
||||
"1m" | "m" | "minute" | "min" => Some("1m".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1444,7 +1444,7 @@ where
|
||||
let snapshot = self.data.market(date, &intent.symbol);
|
||||
if matches!(
|
||||
self.broker.matching_type(),
|
||||
MatchingType::NextTickBestCounterparty | MatchingType::CounterpartyOffer
|
||||
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer
|
||||
) {
|
||||
let depth = self.data.order_book_depth_on(date, &intent.symbol);
|
||||
if !depth.is_empty() {
|
||||
@@ -1454,7 +1454,7 @@ where
|
||||
let quotes = self.data.execution_quotes_on(date, &intent.symbol);
|
||||
for quote in quotes {
|
||||
let price = match self.broker.matching_type() {
|
||||
MatchingType::NextTickBestOwn => match intent.side() {
|
||||
MatchingType::NextMinuteBestOwn => match intent.side() {
|
||||
OrderSide::Buy => {
|
||||
if quote.bid1.is_finite() && quote.bid1 > 0.0 {
|
||||
quote.bid1
|
||||
@@ -1470,7 +1470,7 @@ where
|
||||
}
|
||||
}
|
||||
},
|
||||
MatchingType::NextTickBestCounterparty | MatchingType::CounterpartyOffer => {
|
||||
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer => {
|
||||
match intent.side() {
|
||||
OrderSide::Buy => quote.buy_price().unwrap_or(quote.last_price),
|
||||
OrderSide::Sell => quote.sell_price().unwrap_or(quote.last_price),
|
||||
@@ -2233,9 +2233,9 @@ where
|
||||
"bar:post",
|
||||
)?;
|
||||
|
||||
if should_run_tick_events(&schedule_rules, &self.subscriptions) {
|
||||
if should_run_minute_events(&schedule_rules, &self.subscriptions) {
|
||||
let filter_by_subscription = !self.subscriptions.is_empty();
|
||||
let tick_quotes = self
|
||||
let minute_quotes = self
|
||||
.data
|
||||
.execution_quotes_on_date(execution_date)
|
||||
.into_iter()
|
||||
@@ -2243,9 +2243,9 @@ where
|
||||
!filter_by_subscription || self.subscriptions.contains("e.symbol)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for quote in tick_quotes {
|
||||
let tick_time = quote.timestamp.time();
|
||||
let tick_open_orders = self.open_order_views();
|
||||
for quote in minute_quotes {
|
||||
let minute_time = quote.timestamp.time();
|
||||
let minute_open_orders = self.open_order_views();
|
||||
publish_phase_event(
|
||||
&mut self.strategy,
|
||||
&mut self.process_event_bus,
|
||||
@@ -2255,35 +2255,35 @@ where
|
||||
&self.data,
|
||||
&portfolio,
|
||||
self.futures_account.as_ref(),
|
||||
&tick_open_orders,
|
||||
&minute_open_orders,
|
||||
self.dynamic_universe.as_ref(),
|
||||
&self.subscriptions,
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::PreTick,
|
||||
format!("tick:{}:{}:pre", quote.symbol, quote.timestamp),
|
||||
ProcessEventKind::PreMinute,
|
||||
format!("minute:{}:{}:pre", quote.symbol, quote.timestamp),
|
||||
)?;
|
||||
let mut tick_decision = collect_scheduled_decisions(
|
||||
let mut minute_decision = collect_scheduled_decisions(
|
||||
&mut self.strategy,
|
||||
&scheduler,
|
||||
execution_date,
|
||||
ScheduleStage::Tick,
|
||||
ScheduleStage::Minute,
|
||||
&schedule_rules,
|
||||
decision_date,
|
||||
decision_index,
|
||||
&self.data,
|
||||
&portfolio,
|
||||
self.futures_account.as_ref(),
|
||||
&tick_open_orders,
|
||||
&minute_open_orders,
|
||||
self.dynamic_universe.as_ref(),
|
||||
&self.subscriptions,
|
||||
&mut process_events,
|
||||
&mut self.process_event_bus,
|
||||
Some(tick_time),
|
||||
Some(minute_time),
|
||||
result.order_events.as_slice(),
|
||||
result.fills.as_slice(),
|
||||
)?;
|
||||
tick_decision.merge_from(self.strategy.on_tick(
|
||||
minute_decision.merge_from(self.strategy.on_minute(
|
||||
&StrategyContext {
|
||||
execution_date,
|
||||
decision_date,
|
||||
@@ -2291,7 +2291,7 @@ where
|
||||
data: &self.data,
|
||||
portfolio: &portfolio,
|
||||
futures_account: self.futures_account.as_ref(),
|
||||
open_orders: &tick_open_orders,
|
||||
open_orders: &minute_open_orders,
|
||||
dynamic_universe: self.dynamic_universe.as_ref(),
|
||||
subscriptions: &self.subscriptions,
|
||||
process_events: &process_events,
|
||||
@@ -2311,42 +2311,42 @@ where
|
||||
&self.data,
|
||||
&portfolio,
|
||||
self.futures_account.as_ref(),
|
||||
&tick_open_orders,
|
||||
&minute_open_orders,
|
||||
self.dynamic_universe.as_ref(),
|
||||
&self.subscriptions,
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::Tick,
|
||||
format!("tick:{}:{}", quote.symbol, quote.timestamp),
|
||||
ProcessEventKind::Minute,
|
||||
format!("minute:{}:{}", quote.symbol, quote.timestamp),
|
||||
)?;
|
||||
self.apply_strategy_directives(
|
||||
execution_date,
|
||||
decision_date,
|
||||
decision_index,
|
||||
&mut portfolio,
|
||||
&tick_open_orders,
|
||||
&minute_open_orders,
|
||||
&mut process_events,
|
||||
&mut tick_decision,
|
||||
&mut minute_decision,
|
||||
&mut directive_report,
|
||||
)?;
|
||||
let pre_tick_execution_orders = self.open_order_views();
|
||||
let pre_minute_execution_orders = self.open_order_views();
|
||||
self.ensure_execution_quotes_for_decision(
|
||||
execution_date,
|
||||
&portfolio,
|
||||
&pre_tick_execution_orders,
|
||||
&tick_decision,
|
||||
Some(tick_time),
|
||||
Some(tick_time),
|
||||
&pre_minute_execution_orders,
|
||||
&minute_decision,
|
||||
Some(minute_time),
|
||||
Some(minute_time),
|
||||
)?;
|
||||
let mut tick_report = self.broker.execute_between(
|
||||
let mut minute_report = self.broker.execute_between(
|
||||
execution_date,
|
||||
&mut portfolio,
|
||||
&self.data,
|
||||
&tick_decision,
|
||||
Some(tick_time),
|
||||
Some(tick_time),
|
||||
&minute_decision,
|
||||
Some(minute_time),
|
||||
Some(minute_time),
|
||||
)?;
|
||||
let post_tick_open_orders = self.open_order_views();
|
||||
let post_minute_open_orders = self.open_order_views();
|
||||
publish_process_events(
|
||||
&mut self.strategy,
|
||||
&mut self.process_event_bus,
|
||||
@@ -2356,13 +2356,13 @@ where
|
||||
&self.data,
|
||||
&portfolio,
|
||||
self.futures_account.as_ref(),
|
||||
&post_tick_open_orders,
|
||||
&post_minute_open_orders,
|
||||
self.dynamic_universe.as_ref(),
|
||||
&self.subscriptions,
|
||||
&mut process_events,
|
||||
&mut tick_report.process_events,
|
||||
&mut minute_report.process_events,
|
||||
)?;
|
||||
merge_broker_report(&mut report, tick_report);
|
||||
merge_broker_report(&mut report, minute_report);
|
||||
publish_phase_event(
|
||||
&mut self.strategy,
|
||||
&mut self.process_event_bus,
|
||||
@@ -2372,13 +2372,13 @@ where
|
||||
&self.data,
|
||||
&portfolio,
|
||||
self.futures_account.as_ref(),
|
||||
&post_tick_open_orders,
|
||||
&post_minute_open_orders,
|
||||
self.dynamic_universe.as_ref(),
|
||||
&self.subscriptions,
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::PostTick,
|
||||
format!("tick:{}:{}:post", quote.symbol, quote.timestamp),
|
||||
ProcessEventKind::PostMinute,
|
||||
format!("minute:{}:{}:post", quote.symbol, quote.timestamp),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -3709,7 +3709,7 @@ fn stage_label(stage: ScheduleStage) -> &'static str {
|
||||
ScheduleStage::BeforeTrading => "before_trading",
|
||||
ScheduleStage::OpenAuction => "open_auction",
|
||||
ScheduleStage::Bar => "bar",
|
||||
ScheduleStage::Tick => "tick",
|
||||
ScheduleStage::Minute => "minute",
|
||||
ScheduleStage::OnDay => "on_day",
|
||||
ScheduleStage::AfterTrading => "after_trading",
|
||||
ScheduleStage::Settlement => "settlement",
|
||||
@@ -3723,8 +3723,8 @@ fn stage_datetime(
|
||||
time.map(|value| date.and_time(value))
|
||||
}
|
||||
|
||||
fn should_run_tick_events(rules: &[ScheduleRule], subscriptions: &BTreeSet<String>) -> bool {
|
||||
!subscriptions.is_empty() || rules.iter().any(|rule| rule.stage == ScheduleStage::Tick)
|
||||
fn should_run_minute_events(rules: &[ScheduleRule], subscriptions: &BTreeSet<String>) -> bool {
|
||||
!subscriptions.is_empty() || rules.iter().any(|rule| rule.stage == ScheduleStage::Minute)
|
||||
}
|
||||
|
||||
fn merge_broker_report(target: &mut BrokerExecutionReport, incoming: BrokerExecutionReport) {
|
||||
|
||||
@@ -123,9 +123,9 @@ pub enum ProcessEventKind {
|
||||
PreBar,
|
||||
Bar,
|
||||
PostBar,
|
||||
PreTick,
|
||||
Tick,
|
||||
PostTick,
|
||||
PreMinute,
|
||||
Minute,
|
||||
PostMinute,
|
||||
PreScheduled,
|
||||
PostScheduled,
|
||||
PreOnDay,
|
||||
@@ -165,9 +165,9 @@ impl ProcessEventKind {
|
||||
Self::PreBar => "pre_bar",
|
||||
Self::Bar => "bar",
|
||||
Self::PostBar => "post_bar",
|
||||
Self::PreTick => "pre_tick",
|
||||
Self::Tick => "tick",
|
||||
Self::PostTick => "post_tick",
|
||||
Self::PreMinute => "pre_minute",
|
||||
Self::Minute => "minute",
|
||||
Self::PostMinute => "post_minute",
|
||||
Self::PreScheduled => "pre_scheduled",
|
||||
Self::PostScheduled => "post_scheduled",
|
||||
Self::PreOnDay => "pre_on_day",
|
||||
|
||||
@@ -286,7 +286,7 @@ fn band_low(index_close) {
|
||||
stamp_tax_rate_after_change: None,
|
||||
strict_value_budget: false,
|
||||
slippage_model: SlippageModel::None,
|
||||
matching_type: MatchingType::NextTickLast,
|
||||
matching_type: MatchingType::NextMinuteLast,
|
||||
quote_quantity_limit: true,
|
||||
current_day_precomputed_factors: false,
|
||||
prefer_precomputed_rolling_factors: false,
|
||||
@@ -1343,7 +1343,7 @@ impl PlatformExprStrategy {
|
||||
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 {
|
||||
MatchingType::NextMinuteBestOwn => match side {
|
||||
OrderSide::Buy => (quote.bid1.is_finite() && quote.bid1 > 0.0)
|
||||
.then_some(quote.bid1)
|
||||
.or_else(last),
|
||||
@@ -1351,18 +1351,17 @@ impl PlatformExprStrategy {
|
||||
.then_some(quote.ask1)
|
||||
.or_else(last),
|
||||
},
|
||||
MatchingType::NextTickBestCounterparty | MatchingType::CounterpartyOffer => {
|
||||
MatchingType::NextMinuteBestCounterparty | 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 {
|
||||
MatchingType::NextMinuteLast | 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(),
|
||||
@@ -8428,7 +8427,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn platform_aiquant_next_tick_last_projection_uses_last_quote_for_buy_budget() {
|
||||
fn platform_aiquant_next_minute_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(
|
||||
@@ -8535,7 +8534,7 @@ mod tests {
|
||||
cfg.minimum_commission = Some(5.0);
|
||||
cfg.strict_value_budget = true;
|
||||
cfg.slippage_model = SlippageModel::PriceRatio(0.002);
|
||||
cfg.matching_type = MatchingType::NextTickLast;
|
||||
cfg.matching_type = MatchingType::NextMinuteLast;
|
||||
cfg.quote_quantity_limit = false;
|
||||
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time"));
|
||||
let strategy = PlatformExprStrategy::new(cfg);
|
||||
@@ -8668,7 +8667,7 @@ mod tests {
|
||||
);
|
||||
assert!(
|
||||
18_600.0 * 6.5369 < target_value,
|
||||
"fixture must be below target if tick price is used incorrectly"
|
||||
"fixture must be below target if the minimum price unit is used incorrectly"
|
||||
);
|
||||
|
||||
let ctx = StrategyContext {
|
||||
@@ -8693,7 +8692,7 @@ mod tests {
|
||||
cfg.minimum_commission = Some(5.0);
|
||||
cfg.strict_value_budget = true;
|
||||
cfg.slippage_model = SlippageModel::PriceRatio(0.002);
|
||||
cfg.matching_type = MatchingType::NextTickLast;
|
||||
cfg.matching_type = MatchingType::NextMinuteLast;
|
||||
cfg.quote_quantity_limit = false;
|
||||
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time"));
|
||||
let strategy = PlatformExprStrategy::new(cfg);
|
||||
@@ -21321,7 +21320,7 @@ mod tests {
|
||||
|
||||
assert!(
|
||||
strategy.stock_filter_uses_intraday_quote_fields(),
|
||||
"actual tick/quote fields still require intraday selection quotes"
|
||||
"actual minute quote fields still require intraday selection quotes"
|
||||
);
|
||||
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
@@ -21334,7 +21333,7 @@ mod tests {
|
||||
);
|
||||
assert!(
|
||||
strategy.selection_uses_intraday_quote_fields(),
|
||||
"selection ranking by tick fields must still request intraday selection quotes"
|
||||
"selection ranking by minute fields must still request intraday selection quotes"
|
||||
);
|
||||
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
@@ -21490,7 +21489,7 @@ mod tests {
|
||||
"000003.SZ".to_string(),
|
||||
"000004.SZ".to_string(),
|
||||
],
|
||||
"daily close filter should run before tick quote prefetch; limit state remains a quote-time check"
|
||||
"daily close filter should run before minute quote prefetch; limit state remains a quote-time check"
|
||||
);
|
||||
assert_eq!(
|
||||
plan.order_symbols,
|
||||
|
||||
@@ -429,11 +429,9 @@ fn parse_matching_type(value: Option<&str>) -> Option<MatchingType> {
|
||||
"open_auction" => Some(MatchingType::OpenAuction),
|
||||
"current_bar_close" => Some(MatchingType::CurrentBarClose),
|
||||
"next_bar_open" => Some(MatchingType::NextBarOpen),
|
||||
"next_minute_last" | "next_tick_last" => Some(MatchingType::NextTickLast),
|
||||
"next_minute_best_own" | "next_tick_best_own" => Some(MatchingType::NextTickBestOwn),
|
||||
"next_minute_best_counterparty" | "next_tick_best_counterparty" => {
|
||||
Some(MatchingType::NextTickBestCounterparty)
|
||||
}
|
||||
"next_minute_last" => Some(MatchingType::NextMinuteLast),
|
||||
"next_minute_best_own" => Some(MatchingType::NextMinuteBestOwn),
|
||||
"next_minute_best_counterparty" => Some(MatchingType::NextMinuteBestCounterparty),
|
||||
"counterparty_offer" => Some(MatchingType::CounterpartyOffer),
|
||||
"vwap" => Some(MatchingType::Vwap),
|
||||
"twap" => Some(MatchingType::Twap),
|
||||
@@ -1695,7 +1693,7 @@ mod tests {
|
||||
|
||||
let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
|
||||
|
||||
assert_eq!(cfg.matching_type, MatchingType::NextTickLast);
|
||||
assert_eq!(cfg.matching_type, MatchingType::NextMinuteLast);
|
||||
assert_eq!(cfg.slippage_model, SlippageModel::PriceRatio(0.001));
|
||||
assert!(cfg.strict_value_budget);
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ impl ChinaAShareRiskControl {
|
||||
return Some("paused");
|
||||
}
|
||||
// `allow_sell` is derived from the daily candidate snapshot and may
|
||||
// reflect an open/close fallback rather than the actual execution tick.
|
||||
// reflect an open/close fallback rather than the actual execution price.
|
||||
// A sell order must be blocked by the execution price lower-limit check
|
||||
// below, while suspension and delisting are handled above.
|
||||
if market.is_at_lower_limit_price(check_price) {
|
||||
|
||||
@@ -7,7 +7,7 @@ pub enum ScheduleStage {
|
||||
BeforeTrading,
|
||||
OpenAuction,
|
||||
Bar,
|
||||
Tick,
|
||||
Minute,
|
||||
OnDay,
|
||||
AfterTrading,
|
||||
Settlement,
|
||||
@@ -225,7 +225,7 @@ pub fn default_stage_time(stage: ScheduleStage) -> Option<NaiveTime> {
|
||||
ScheduleStage::BeforeTrading => Some(NaiveTime::from_hms_opt(9, 0, 0).expect("valid time")),
|
||||
ScheduleStage::OpenAuction => Some(NaiveTime::from_hms_opt(9, 31, 0).expect("valid time")),
|
||||
ScheduleStage::Bar => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
|
||||
ScheduleStage::Tick => None,
|
||||
ScheduleStage::Minute => None,
|
||||
ScheduleStage::OnDay => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
|
||||
ScheduleStage::AfterTrading => Some(NaiveTime::from_hms_opt(15, 0, 0).expect("valid time")),
|
||||
ScheduleStage::Settlement => Some(NaiveTime::from_hms_opt(15, 1, 0).expect("valid time")),
|
||||
|
||||
@@ -68,7 +68,7 @@ pub trait Strategy {
|
||||
fn on_bar(&mut self, _ctx: &StrategyContext<'_>) -> Result<StrategyDecision, BacktestError> {
|
||||
Ok(StrategyDecision::default())
|
||||
}
|
||||
fn on_tick(
|
||||
fn on_minute(
|
||||
&mut self,
|
||||
_ctx: &StrategyContext<'_>,
|
||||
_quote: &IntradayExecutionQuote,
|
||||
|
||||
@@ -97,7 +97,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
"平台策略脚本采用声明式 DSL + 表达式执行模型。".to_string(),
|
||||
"支持 let 变量、fn 自定义函数、when/unless/else 条件块、可用指标/因子字段映射。".to_string(),
|
||||
"支持数值型和字符串型因子,字符串字段可用于行业、概念、标签、板块等分类过滤。".to_string(),
|
||||
"当前默认回测数据已支持 OHLCV、市值、流通市值、换手率、有效换手率、上市天数、停牌/ST/板块、涨跌停价格、tick 触达涨跌停、常用价格/成交量均线,以及 stock_indicator_factors_v1 中已入库的通用指标因子。".to_string(),
|
||||
"当前默认回测数据已支持 OHLCV、市值、流通市值、换手率、有效换手率、上市天数、停牌/ST/板块、涨跌停价格、分钟线触达涨跌停、常用价格/成交量均线,以及 stock_indicator_factors_v1 中已入库的通用指标因子。".to_string(),
|
||||
"AI 生成策略时只能输出完整 engine-script 代码,不输出 Markdown、解释、推理过程、JSON 包装或手册复述。".to_string(),
|
||||
"表达式字段以运行时字段为准:市值使用 market_cap,流通市值使用 free_float_cap;不要在策略表达式中使用数据库原始字段 float_market_cap。".to_string(),
|
||||
"任意窗口价格均线使用 rolling_mean(\"close\", n) 或 ma(\"close\", n),任意窗口均量使用 rolling_mean(\"volume\", n) 或 vma(n);不要使用未列出的 ma60、stock_ma60、signal_ma60 或 benchmark_ma60 变量。".to_string(),
|
||||
@@ -204,7 +204,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
},
|
||||
ManualSection {
|
||||
title: "bar / minute execution 生命周期".to_string(),
|
||||
detail: "回测内核支持 平台内核 风格的 bar/分钟执行价生命周期:日内会发布 pre_bar/bar/post_bar 过程事件;存在分钟执行价订阅或分钟调度规则时,会按 execution_quotes 的时间顺序发布 pre_tick/tick/post_tick 兼容事件,并把日内阶段下单限制在当前分钟执行价时间窗内撮合。平台 DSL 中可通过 subscribe([...])、trading.subscription_guard(true) 和 process_event 字段配合显式订单模拟日内订阅策略。".to_string(),
|
||||
detail: "回测内核支持 平台内核 风格的 bar/分钟执行价生命周期:日内会发布 pre_bar/bar/post_bar 过程事件;存在分钟执行价订阅或分钟调度规则时,会按 execution_quotes 的时间顺序发布 pre_minute/minute/post_minute 过程事件,并把日内阶段下单限制在当前分钟执行价时间窗内撮合。平台 DSL 中可通过 subscribe([...])、trading.subscription_guard(true) 和 process_event 字段配合显式订单模拟日内订阅策略。".to_string(),
|
||||
},
|
||||
ManualSection {
|
||||
title: "selection.market_cap_band / selection.limit / ordering.rank_by / ordering.rank_expr".to_string(),
|
||||
@@ -220,11 +220,11 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
},
|
||||
ManualSection {
|
||||
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_tick_* 仅作为兼容别名,内部仍读取分钟执行价。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(\"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(),
|
||||
},
|
||||
ManualSection {
|
||||
title: "期货提交校验".to_string(),
|
||||
detail: "期货订单进入撮合前会先执行账户与交易规则校验:合约必须在上市/退市日期范围内,日行情不能停牌,trading_phase 需处于 continuous/trading/open_auction/auction/call_auction/opening_auction 等可交易阶段,限价必须为正且按 futures_trading_parameters.price_tick 或日行情 price_tick 对齐,并且不能越过 upper_limit/lower_limit;随后继续检查反向挂单自成交风险、保证金和可平数量。服务层可通过 FuturesValidationConfig 分别关闭 active instrument、trading phase、limit price tick、price limit 校验,用于兼容特殊数据,但默认全部开启。".to_string(),
|
||||
detail: "期货订单进入撮合前会先执行账户与交易规则校验:合约必须在上市/退市日期范围内,日行情不能停牌,trading_phase 需处于 continuous/trading/open_auction/auction/call_auction/opening_auction 等可交易阶段,限价必须为正且按 futures_trading_parameters.price_tick 或日行情 price_tick 对齐,并且不能越过 upper_limit/lower_limit;随后继续检查反向挂单自成交风险、保证金和可平数量。服务层可通过 FuturesValidationConfig 分别关闭 active instrument、trading phase、限价最小价位、price limit 校验,但默认全部开启。".to_string(),
|
||||
},
|
||||
ManualSection {
|
||||
title: "trading.rotation / order.* / cancel.* / update_universe / subscribe".to_string(),
|
||||
@@ -267,7 +267,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
fields: vec![
|
||||
ManualField { name: "symbol".to_string(), field_type: "string".to_string(), detail: "证券代码。".to_string() },
|
||||
ManualField { name: "market_cap/free_float_cap".to_string(), field_type: "float".to_string(), detail: "总市值、流通市值。".to_string() },
|
||||
ManualField { name: "turnover/turnover_ratio/effective_turnover_ratio".to_string(), field_type: "float".to_string(), detail: "换手率、换手率标准字段、有效换手率;turnover 是 turnover_ratio 的兼容别名。".to_string() },
|
||||
ManualField { name: "turnover_ratio/effective_turnover_ratio".to_string(), field_type: "float".to_string(), detail: "换手率标准字段和有效换手率。".to_string() },
|
||||
ManualField { name: "open/high/low/close/last/last_price/prev_close/amount".to_string(), field_type: "float".to_string(), detail: "开盘、最高、最低、收盘、盘中价、昨收和成交额。".to_string() },
|
||||
ManualField { name: "upper_limit/lower_limit/price_tick/round_lot/minimum_order_quantity/order_step_size".to_string(), field_type: "float/int".to_string(), detail: "涨跌停、最小价位、整手、最小下单量和数量步长。KSH/BJSE 等板块可与 round_lot 不同。".to_string() },
|
||||
ManualField { name: "paused/is_st/is_kcb/is_one_yuan/is_new_listing".to_string(), field_type: "bool".to_string(), detail: "可交易性与板块标志。".to_string() },
|
||||
@@ -304,13 +304,13 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
functions: vec![
|
||||
ManualFunction { name: "factor".to_string(), signature: "factor(\"column_name\")".to_string(), detail: "读取当前股票当日可用因子列。数值因子返回 float,字符串因子返回 string;缺失字段默认返回 0 或空字符串,建议重要条件配合 diagnostics 查看候选过滤数量。".to_string() },
|
||||
ManualFunction { name: "day_factor".to_string(), signature: "day_factor(\"field_name\")".to_string(), detail: "读取日级/指数级字段映射。".to_string() },
|
||||
ManualFunction { name: "history_bars".to_string(), signature: "ctx.history_bars(symbol, count, \"1d\" | \"1m\", \"close\", include_now)".to_string(), detail: "回测内核策略上下文数据 API,返回指定证券最近 N 条数值序列。日线字段支持 open/high/low/close/last/prev_close/volume/upper_limit/lower_limit;分钟字段支持 last/bid1/ask1/volume_delta/amount_delta。旧 \"tick\" 频率只作为兼容别名映射到 \"1m\",不再表示逐笔数据。日线 include_now=false 排除当前交易日;分钟线会按当前 on_bar、日内事件或调度时刻截断,include_now=false 排除当前分钟执行价,避免未来函数。".to_string() },
|
||||
ManualFunction { name: "history_bars".to_string(), signature: "ctx.history_bars(symbol, count, \"1d\" | \"1m\", \"close\", include_now)".to_string(), detail: "回测内核策略上下文数据 API,返回指定证券最近 N 条数值序列。日线字段支持 open/high/low/close/last/prev_close/volume/upper_limit/lower_limit;分钟字段支持 last/bid1/ask1/volume_delta/amount_delta。日线 include_now=false 排除当前交易日;分钟线会按当前 on_bar、日内事件或调度时刻截断,include_now=false 排除当前分钟执行价,避免未来函数。".to_string() },
|
||||
ManualFunction { name: "current_snapshot".to_string(), signature: "ctx.current_snapshot(symbol)".to_string(), detail: "读取当前交易日指定证券的日级快照,可用于获得当日 open/close/last/upper_limit/lower_limit 等字段。".to_string() },
|
||||
ManualFunction { name: "instrument/instruments/all_instruments".to_string(), signature: "ctx.instrument(symbol)".to_string(), detail: "读取证券元数据,包括名称、板块、上市日期、退市日期、最小下单量、整手、最小价位等;all_instruments 按证券代码稳定排序返回全量证券。".to_string() },
|
||||
ManualFunction { name: "active_instruments/instruments_history".to_string(), signature: "ctx.active_instruments(&[symbol])".to_string(), detail: "active_instruments 返回当前交易日已上市且未退市的证券;instruments_history 返回给定代码的历史证券记录,包含当前已退市标的,对齐 平台内核 的 active_instruments/instruments_history 能力。".to_string() },
|
||||
ManualFunction { name: "get_trading_dates/get_previous_trading_date/get_next_trading_date".to_string(), signature: "ctx.get_previous_trading_date(date, n)".to_string(), detail: "交易日历 API。get_trading_dates 返回闭区间交易日;previous/next 返回相对某日向前或向后的第 n 个交易日,当前日自身不计入。".to_string() },
|
||||
ManualFunction { name: "is_suspended/is_st_stock".to_string(), signature: "ctx.is_suspended(symbol, count)".to_string(), detail: "读取指定证券截至当前交易日最近 count 个交易日的停牌或 ST 标记,返回 bool 序列,顺序从旧到新;对应平台内核的 is_suspended/is_st_stock 数据能力。".to_string() },
|
||||
ManualFunction { name: "get_price".to_string(), signature: "ctx.get_price(symbol, start_date, end_date, \"1d\" | \"1m\" | \"tick\")".to_string(), detail: "按日期区间读取统一 PriceBar 序列。日线返回 open/high/low/close/last/volume/盘口字段;分钟或 tick 返回按 timestamp 排序的 last/bid1/ask1/volume_delta/amount_delta 映射,便于服务层转成表格或前端明细。".to_string() },
|
||||
ManualFunction { name: "get_price".to_string(), signature: "ctx.get_price(symbol, start_date, end_date, \"1d\" | \"1m\")".to_string(), detail: "按日期区间读取统一 PriceBar 序列。日线返回 open/high/low/close/last/volume/盘口字段;分钟线返回按 timestamp 排序的 last/bid1/ask1/volume_delta/amount_delta 映射,便于服务层转成表格或前端明细。".to_string() },
|
||||
ManualFunction { name: "get_dividend / dividend_cash / has_dividend".to_string(), signature: "dividend_cash(lookback) / has_dividend(lookback)".to_string(), detail: "高级数据 风格分红 API。Rust Context 可用 ctx.get_dividend(symbol, start_date) 读取明细;平台表达式可用 dividend_cash(lookback) 汇总当前股票最近 N 个交易日现金分红,用 has_dividend(lookback) 判断是否发生分红,也支持 dividend_cash(\"600000.SH\", lookback)。".to_string() },
|
||||
ManualFunction { name: "get_split / split_ratio / has_split".to_string(), signature: "split_ratio(lookback) / has_split(lookback)".to_string(), detail: "高级数据 风格拆分/送转 API。Rust Context 可用 ctx.get_split(symbol, start_date) 读取明细;平台表达式可用 split_ratio(lookback) 计算当前股票最近 N 个交易日累计拆分比例,has_split(lookback) 判断是否发生送转。".to_string() },
|
||||
ManualFunction { name: "get_factor / factor_value".to_string(), signature: "factor_value(\"field\", lookback=1)".to_string(), detail: "数值因子 API。factor(\"field\") 读取当前股票当日因子;factor_value(\"field\", lookback) 会在最近 N 个交易日内取该字段最新数值,适合读取任意可用指标或自定义数值因子。Rust Context 可用 ctx.get_factor(symbol, start, end, field) 读取完整数值序列。".to_string() },
|
||||
@@ -365,7 +365,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
},
|
||||
ManualFactorSource {
|
||||
table: "期货交易参数".to_string(),
|
||||
detail: "字段包括 symbol、effective_date、contract_multiplier、long_margin_rate、short_margin_rate、commission_type、open_commission_ratio、close_commission_ratio、close_today_commission_ratio、price_tick。回测会按交易日自动选择不晚于当前日期的最新参数,用于保证金、手续费和限价 tick 校验。".to_string(),
|
||||
detail: "字段包括 symbol、effective_date、contract_multiplier、long_margin_rate、short_margin_rate、commission_type、open_commission_ratio、close_commission_ratio、close_today_commission_ratio、price_tick。回测会按交易日自动选择不晚于当前日期的最新参数,用于保证金、手续费和限价最小价位校验。".to_string(),
|
||||
fields: vec![],
|
||||
},
|
||||
],
|
||||
@@ -518,7 +518,7 @@ pub fn build_generation_prompt(
|
||||
prompt.push_str("- 生成的代码必须能转换为 strategy_spec 并提交 POST /v1/backtests。\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("参数形态必须严格: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_tick_* 只是兼容别名,新生成策略不要使用;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 只能取 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("回测成功但 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("用户目标:\n");
|
||||
|
||||
Reference in New Issue
Block a user