增加类型化静态股票池合同
This commit is contained in:
@@ -596,6 +596,7 @@ pub struct PlatformExprStrategyConfig {
|
||||
pub refresh_rate_expr: String,
|
||||
pub max_positions: usize,
|
||||
pub prelude: String,
|
||||
pub universe_include: Option<BTreeSet<String>>,
|
||||
pub universe_exclude: Vec<String>,
|
||||
pub market_cap_field: String,
|
||||
pub market_cap_lower_expr: String,
|
||||
@@ -673,6 +674,7 @@ impl PlatformExprStrategyConfig {
|
||||
refresh_rate_expr: String::new(),
|
||||
max_positions: 1,
|
||||
prelude: String::new(),
|
||||
universe_include: None,
|
||||
universe_exclude: Vec::new(),
|
||||
market_cap_field: "market_cap".to_string(),
|
||||
market_cap_lower_expr: "0.0".to_string(),
|
||||
@@ -9974,6 +9976,14 @@ impl PlatformExprStrategy {
|
||||
let factor_symbol_ids = factor_day.factor_symbol_ids();
|
||||
debug_assert_eq!(factor_rows.len(), factor_symbol_ids.len());
|
||||
for (factor, symbol_id) in factor_rows.iter().zip(factor_symbol_ids.iter().copied()) {
|
||||
if self
|
||||
.config
|
||||
.universe_include
|
||||
.as_ref()
|
||||
.is_some_and(|symbols| !symbols.contains(&factor.symbol))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !self.config.candidate_symbols_by_date.is_empty()
|
||||
&& !self
|
||||
.config
|
||||
@@ -10495,6 +10505,14 @@ impl PlatformExprStrategy {
|
||||
let market = ctx.data.require_market(date, symbol)?;
|
||||
let candidate = ctx.data.require_candidate(date, symbol)?;
|
||||
|
||||
if self
|
||||
.config
|
||||
.universe_include
|
||||
.as_ref()
|
||||
.is_some_and(|symbols| !symbols.contains(&market.symbol))
|
||||
{
|
||||
return Ok(Some("universe_not_included".to_string()));
|
||||
}
|
||||
if let Some(reason) =
|
||||
Self::universe_exclude_reason(&self.config.universe_exclude, &market.symbol)
|
||||
{
|
||||
@@ -24352,6 +24370,26 @@ mod tests {
|
||||
assert_eq!(rejection.as_deref(), Some("paused"));
|
||||
assert_eq!(selected.len(), 1);
|
||||
assert_eq!(selected[0].symbol, symbol);
|
||||
|
||||
let mut restricted_cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
restricted_cfg.universe_include = Some(BTreeSet::from(["000001.SZ".to_string()]));
|
||||
restricted_cfg.universe_exclude.clear();
|
||||
let restricted_strategy = PlatformExprStrategy::new(restricted_cfg);
|
||||
let restricted_stock = restricted_strategy
|
||||
.stock_state(&ctx, date, symbol)
|
||||
.expect("restricted stock state");
|
||||
assert_eq!(
|
||||
restricted_strategy
|
||||
.buy_rejection_reason(&ctx, date, symbol, &restricted_stock)
|
||||
.expect("static universe rejection")
|
||||
.as_deref(),
|
||||
Some("universe_not_included")
|
||||
);
|
||||
assert!(
|
||||
restricted_strategy
|
||||
.selectable_universe_on(&ctx, date, date)
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
fn sample_calendar() -> TradingCalendar {
|
||||
|
||||
@@ -62,6 +62,8 @@ pub struct StrategyBenchmarkSpec {
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StrategyUniverseSpec {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub include: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub exclude: Vec<String>,
|
||||
}
|
||||
@@ -1944,6 +1946,27 @@ pub fn platform_expr_config_from_spec(
|
||||
}
|
||||
}
|
||||
if let Some(universe) = spec.universe.as_ref() {
|
||||
cfg.universe_include = universe
|
||||
.include
|
||||
.as_ref()
|
||||
.map(|raw_symbols| {
|
||||
let mut symbols = BTreeSet::new();
|
||||
for raw_symbol in raw_symbols {
|
||||
let symbol = normalize_symbol(raw_symbol, None);
|
||||
if !is_static_cn_universe_symbol(&symbol) {
|
||||
return Err(format!(
|
||||
"universe.include contains invalid CN stock symbol: {raw_symbol}"
|
||||
));
|
||||
}
|
||||
if !symbols.insert(symbol.clone()) {
|
||||
return Err(format!(
|
||||
"universe.include contains duplicate normalized symbol: {symbol}"
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(symbols)
|
||||
})
|
||||
.transpose()?;
|
||||
cfg.universe_exclude = universe
|
||||
.exclude
|
||||
.iter()
|
||||
@@ -3009,6 +3032,15 @@ fn normalize_symbol(symbol: &str, raw_board: Option<&str>) -> String {
|
||||
instrument_query_id(trimmed, &normalize_board(trimmed, raw_board))
|
||||
}
|
||||
|
||||
fn is_static_cn_universe_symbol(symbol: &str) -> bool {
|
||||
let Some((code, exchange)) = symbol.rsplit_once('.') else {
|
||||
return false;
|
||||
};
|
||||
code.len() == 6
|
||||
&& code.bytes().all(|byte| byte.is_ascii_digit())
|
||||
&& matches!(exchange, "SH" | "SZ" | "BJ")
|
||||
}
|
||||
|
||||
fn instrument_query_id(symbol: &str, board: &str) -> String {
|
||||
if symbol.contains('.') {
|
||||
return symbol.to_ascii_uppercase();
|
||||
@@ -3095,7 +3127,10 @@ mod tests {
|
||||
"strategyId": "runtime_spec_test",
|
||||
"signalSymbol": "000852.SH",
|
||||
"benchmark": { "instrumentId": "000852.SH" },
|
||||
"universe": { "exclude": ["paused", "st", "kcb", "one_yuan"] },
|
||||
"universe": {
|
||||
"include": ["600000.sh", "000001.SZ"],
|
||||
"exclude": ["paused", "st", "kcb", "one_yuan"]
|
||||
},
|
||||
"runtimeExpressions": {
|
||||
"prelude": "let stocknum = 8;",
|
||||
"selection": {
|
||||
@@ -3134,6 +3169,13 @@ mod tests {
|
||||
assert_eq!(cfg.signal_symbol, "000852.SH");
|
||||
assert_eq!(cfg.selection_limit_expr, "stocknum");
|
||||
assert_eq!(cfg.refresh_rate_expr, "year >= 2024 ? 5 : 20");
|
||||
assert_eq!(
|
||||
cfg.universe_include,
|
||||
Some(BTreeSet::from([
|
||||
"000001.SZ".to_string(),
|
||||
"600000.SH".to_string()
|
||||
]))
|
||||
);
|
||||
assert_eq!(cfg.universe_exclude, ["paused", "st", "kcb", "one_yuan"]);
|
||||
assert!(!cfg.rotation_enabled);
|
||||
assert!(cfg.daily_top_up_enabled);
|
||||
@@ -3155,6 +3197,29 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_or_duplicate_static_universe_symbols() {
|
||||
let invalid = serde_json::json!({
|
||||
"universe": {"include": ["not-a-stock"]}
|
||||
});
|
||||
assert!(
|
||||
platform_expr_config_from_value("", "", &invalid)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("invalid CN stock symbol")
|
||||
);
|
||||
|
||||
let duplicate = serde_json::json!({
|
||||
"universe": {"include": ["600000.sh", "600000.SH"]}
|
||||
});
|
||||
assert!(
|
||||
platform_expr_config_from_value("", "", &duplicate)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("duplicate normalized symbol")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_and_rejects_invalid_position_target_rules() {
|
||||
let spec = serde_json::json!({
|
||||
|
||||
Reference in New Issue
Block a user