修正FIDC风控字段与默认选股语义

This commit is contained in:
boris
2026-07-07 13:14:34 +08:00
parent 90857fae0a
commit 6b306eecf2
2 changed files with 223 additions and 6 deletions
+222 -6
View File
@@ -274,7 +274,7 @@ fn band_low(index_close) {
round((index_close - 2000) * 4 / 500 + 7)
}"#
.to_string(),
universe_exclude: vec!["bjse".to_string()],
universe_exclude: Vec::new(),
market_cap_field: "market_cap".to_string(),
market_cap_lower_expr: "band_low(signal_close)".to_string(),
market_cap_upper_expr: "band_low(signal_close) + 10".to_string(),
@@ -439,7 +439,9 @@ struct StockExpressionState {
order_step_size: i64,
paused: bool,
is_st: bool,
is_star_st: bool,
is_kcb: bool,
is_bjse: bool,
is_one_yuan: bool,
is_new_listing: bool,
allow_buy: bool,
@@ -936,7 +938,9 @@ impl PlatformExprStrategy {
| "round_lot"
| "paused"
| "is_st"
| "is_star_st"
| "is_kcb"
| "is_bjse"
| "is_one_yuan"
| "is_new_listing"
| "allow_buy"
@@ -2829,7 +2833,9 @@ impl PlatformExprStrategy {
order_step_size: instrument.map(|item| item.order_step_size()).unwrap_or(100) as i64,
paused: market.paused || candidate.is_paused,
is_st: candidate.is_st,
is_star_st: candidate.is_star_st,
is_kcb: candidate.is_kcb,
is_bjse: Self::symbol_is_bjse(symbol),
is_one_yuan: candidate.is_one_yuan || market.day_open <= 1.0,
is_new_listing: candidate.is_new_listing,
allow_buy: candidate.allow_buy,
@@ -3234,7 +3240,9 @@ impl PlatformExprStrategy {
scope.push("order_step_size", stock.order_step_size);
scope.push("paused", stock.paused);
scope.push("is_st", stock.is_st);
scope.push("is_star_st", stock.is_star_st);
scope.push("is_kcb", stock.is_kcb);
scope.push("is_bjse", stock.is_bjse);
scope.push("is_one_yuan", stock.is_one_yuan);
scope.push("is_new_listing", stock.is_new_listing);
scope.push("allow_buy", stock.allow_buy);
@@ -3344,7 +3352,9 @@ impl PlatformExprStrategy {
);
factors.insert("paused".into(), Dynamic::from(stock.paused));
factors.insert("is_st".into(), Dynamic::from(stock.is_st));
factors.insert("is_star_st".into(), Dynamic::from(stock.is_star_st));
factors.insert("is_kcb".into(), Dynamic::from(stock.is_kcb));
factors.insert("is_bjse".into(), Dynamic::from(stock.is_bjse));
factors.insert("is_one_yuan".into(), Dynamic::from(stock.is_one_yuan));
factors.insert("is_new_listing".into(), Dynamic::from(stock.is_new_listing));
factors.insert("allow_buy".into(), Dynamic::from(stock.allow_buy));
@@ -6444,6 +6454,11 @@ impl PlatformExprStrategy {
None
}
fn symbol_is_bjse(symbol: &str) -> bool {
let normalized = symbol.trim().to_ascii_uppercase();
normalized.ends_with(".BJ") || normalized.ends_with(".BSE") || normalized.ends_with(".BE")
}
fn stock_numeric_field_value(
&self,
candidate: &EligibleUniverseSnapshot,
@@ -6501,7 +6516,11 @@ impl PlatformExprStrategy {
}
"paused" => Some(if stock.paused { 1.0 } else { 0.0 }),
"is_st" => Some(if stock.is_st { 1.0 } else { 0.0 }),
"is_star_st" | "star_st" => Some(if stock.is_star_st { 1.0 } else { 0.0 }),
"is_kcb" => Some(if stock.is_kcb { 1.0 } else { 0.0 }),
"is_bjse" | "bjse" | "bse" | "beijing_exchange" | "north_exchange" => {
Some(if stock.is_bjse { 1.0 } else { 0.0 })
}
"is_one_yuan" => Some(if stock.is_one_yuan { 1.0 } else { 0.0 }),
"is_new_listing" => Some(if stock.is_new_listing { 1.0 } else { 0.0 }),
"candidate_market_cap" => Some(Self::market_cap_storage_to_strategy_unit(
@@ -7135,7 +7154,9 @@ impl PlatformExprStrategy {
| "hit_lower_limit"
| "paused"
| "is_st"
| "is_star_st"
| "is_kcb"
| "is_bjse"
| "is_one_yuan"
| "is_new_listing"
| "candidate_market_cap"
@@ -9095,11 +9116,12 @@ mod tests {
};
use crate::{
AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction,
DailyFactorSnapshot, DailyMarketSnapshot, DataSet, FactorTextValue, FuturesCommissionType,
FuturesTradingParameter, Instrument, IntradayExecutionQuote, MatchingType, OpenOrderView,
OrderIntent, OrderSide, PortfolioState, ProcessEvent, ProcessEventKind, RebalanceCashMode,
ScheduleStage, ScheduleTimeRule, SlippageModel, Strategy, StrategyContext,
TargetPortfolioOrderPricing, TradingCalendar, default_stage_time,
DailyFactorSnapshot, DailyMarketSnapshot, DataSet, EligibleUniverseSnapshot,
FactorTextValue, FuturesCommissionType, FuturesTradingParameter, Instrument,
IntradayExecutionQuote, MatchingType, OpenOrderView, OrderIntent, OrderSide,
PortfolioState, ProcessEvent, ProcessEventKind, RebalanceCashMode, ScheduleStage,
ScheduleTimeRule, SlippageModel, Strategy, StrategyContext, TargetPortfolioOrderPricing,
TradingCalendar, default_stage_time,
};
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
@@ -13707,6 +13729,200 @@ mod tests {
);
}
#[test]
fn platform_stock_expr_exposes_star_st_and_bjse_fields_without_default_selection_risk() {
let date = d(2025, 11, 10);
let signal = "000001.SH";
let star_st_symbol = "300268.SZ";
let bjse_symbol = "920508.BJ";
let normal_symbol = "301167.SZ";
let market = |symbol: &str, close: f64| DailyMarketSnapshot {
date,
symbol: symbol.to_string(),
timestamp: Some(format!("{date} 15:00:00")),
day_open: close,
open: close,
high: close,
low: close,
close,
last_price: close,
bid1: close,
ask1: close,
prev_close: close,
volume: 1_000_000,
minute_volume: 10_000,
bid1_volume: 10_000,
ask1_volume: 10_000,
trading_phase: Some("continuous".to_string()),
paused: false,
upper_limit: close * 1.1,
lower_limit: close * 0.9,
price_tick: 0.01,
};
let symbols = [signal, star_st_symbol, bjse_symbol, normal_symbol];
let data = DataSet::from_components(
symbols
.into_iter()
.map(|symbol| Instrument {
symbol: symbol.to_string(),
name: symbol.to_string(),
board: if symbol.ends_with(".BJ") {
"BJ".to_string()
} else if symbol.ends_with(".SH") {
"SH".to_string()
} else {
"SZ".to_string()
},
round_lot: 100,
listed_at: Some(d(2020, 1, 1)),
delisted_at: None,
status: "active".to_string(),
})
.collect(),
symbols
.into_iter()
.map(|symbol| market(symbol, 10.0))
.collect(),
[star_st_symbol, bjse_symbol, normal_symbol]
.into_iter()
.enumerate()
.map(|(index, symbol)| DailyFactorSnapshot {
date,
symbol: symbol.to_string(),
market_cap_bn: 1.0 + index as f64,
free_float_cap_bn: 1.0 + index as f64,
pe_ttm: 8.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
extra_factors: BTreeMap::new(),
})
.collect(),
vec![
CandidateEligibility {
date,
symbol: star_st_symbol.to_string(),
is_st: true,
is_star_st: true,
is_new_listing: false,
is_paused: false,
allow_buy: true,
allow_sell: true,
is_kcb: false,
is_one_yuan: false,
risk_level_code: None,
},
CandidateEligibility {
date,
symbol: bjse_symbol.to_string(),
is_st: false,
is_star_st: false,
is_new_listing: false,
is_paused: false,
allow_buy: true,
allow_sell: true,
is_kcb: false,
is_one_yuan: false,
risk_level_code: None,
},
CandidateEligibility {
date,
symbol: normal_symbol.to_string(),
is_st: false,
is_star_st: false,
is_new_listing: false,
is_paused: false,
allow_buy: true,
allow_sell: true,
is_kcb: false,
is_one_yuan: false,
risk_level_code: None,
},
],
vec![BenchmarkSnapshot {
date,
benchmark: "000852.SH".to_string(),
open: 1000.0,
close: 1002.0,
prev_close: 998.0,
volume: 1_000_000,
}],
)
.expect("dataset");
let portfolio = PortfolioState::new(1_000_000.0);
let subscriptions = BTreeSet::new();
let ctx = StrategyContext {
execution_date: date,
decision_date: date,
decision_index: 0,
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.signal_symbol = signal.to_string();
cfg.market_cap_lower_expr = "0".to_string();
cfg.market_cap_upper_expr = "100".to_string();
cfg.selection_limit_expr = "2".to_string();
cfg.stock_filter_expr = "is_star_st || is_bjse".to_string();
cfg.benchmark_short_ma_days = 1;
cfg.benchmark_long_ma_days = 1;
let strategy = PlatformExprStrategy::new(cfg);
let day = strategy.day_state(&ctx, date).expect("day state");
assert_eq!(
strategy
.selectable_universe_on(&ctx, date, date)
.iter()
.map(|row| row.symbol.as_str())
.collect::<Vec<_>>(),
vec![star_st_symbol, bjse_symbol, normal_symbol]
);
let (selected, diagnostics, risk_decisions) = strategy
.select_symbols(&ctx, date, date, date, &day, 0.0, 100.0, 2)
.expect("select symbols");
assert!(diagnostics.is_empty(), "{diagnostics:?}");
assert!(risk_decisions.is_empty());
assert_eq!(
selected,
vec![star_st_symbol.to_string(), bjse_symbol.to_string()]
);
let star_stock = strategy
.selection_stock_state_with_factor_date(&ctx, date, date, star_st_symbol)
.expect("star st state");
let bjse_stock = strategy
.selection_stock_state_with_factor_date(&ctx, date, date, bjse_symbol)
.expect("bjse state");
let star_candidate = EligibleUniverseSnapshot {
symbol: star_st_symbol.to_string(),
market_cap_bn: 1.0,
free_float_cap_bn: 1.0,
};
let bjse_candidate = EligibleUniverseSnapshot {
symbol: bjse_symbol.to_string(),
market_cap_bn: 2.0,
free_float_cap_bn: 2.0,
};
assert_eq!(
strategy.stock_numeric_field_value(&star_candidate, &star_stock, "is_star_st"),
Some(1.0)
);
assert_eq!(
strategy.stock_numeric_field_value(&bjse_candidate, &bjse_stock, "is_bjse"),
Some(1.0)
);
}
#[test]
fn platform_next_open_select_symbols_defers_decision_day_limit_state() {
let decision_date = d(2025, 1, 2);
@@ -157,6 +157,7 @@ const RESERVED_SCOPE_NAMES: &[&str] = &[
"is_st",
"is_star_st",
"is_kcb",
"is_bjse",
"is_one_yuan",
"is_new_listing",
"allow_buy",