增加类型化静态股票池合同
This commit is contained in:
@@ -596,6 +596,7 @@ pub struct PlatformExprStrategyConfig {
|
|||||||
pub refresh_rate_expr: String,
|
pub refresh_rate_expr: String,
|
||||||
pub max_positions: usize,
|
pub max_positions: usize,
|
||||||
pub prelude: String,
|
pub prelude: String,
|
||||||
|
pub universe_include: Option<BTreeSet<String>>,
|
||||||
pub universe_exclude: Vec<String>,
|
pub universe_exclude: Vec<String>,
|
||||||
pub market_cap_field: String,
|
pub market_cap_field: String,
|
||||||
pub market_cap_lower_expr: String,
|
pub market_cap_lower_expr: String,
|
||||||
@@ -673,6 +674,7 @@ impl PlatformExprStrategyConfig {
|
|||||||
refresh_rate_expr: String::new(),
|
refresh_rate_expr: String::new(),
|
||||||
max_positions: 1,
|
max_positions: 1,
|
||||||
prelude: String::new(),
|
prelude: String::new(),
|
||||||
|
universe_include: None,
|
||||||
universe_exclude: Vec::new(),
|
universe_exclude: Vec::new(),
|
||||||
market_cap_field: "market_cap".to_string(),
|
market_cap_field: "market_cap".to_string(),
|
||||||
market_cap_lower_expr: "0.0".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();
|
let factor_symbol_ids = factor_day.factor_symbol_ids();
|
||||||
debug_assert_eq!(factor_rows.len(), factor_symbol_ids.len());
|
debug_assert_eq!(factor_rows.len(), factor_symbol_ids.len());
|
||||||
for (factor, symbol_id) in factor_rows.iter().zip(factor_symbol_ids.iter().copied()) {
|
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()
|
if !self.config.candidate_symbols_by_date.is_empty()
|
||||||
&& !self
|
&& !self
|
||||||
.config
|
.config
|
||||||
@@ -10495,6 +10505,14 @@ impl PlatformExprStrategy {
|
|||||||
let market = ctx.data.require_market(date, symbol)?;
|
let market = ctx.data.require_market(date, symbol)?;
|
||||||
let candidate = ctx.data.require_candidate(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) =
|
if let Some(reason) =
|
||||||
Self::universe_exclude_reason(&self.config.universe_exclude, &market.symbol)
|
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!(rejection.as_deref(), Some("paused"));
|
||||||
assert_eq!(selected.len(), 1);
|
assert_eq!(selected.len(), 1);
|
||||||
assert_eq!(selected[0].symbol, symbol);
|
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 {
|
fn sample_calendar() -> TradingCalendar {
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ pub struct StrategyBenchmarkSpec {
|
|||||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct StrategyUniverseSpec {
|
pub struct StrategyUniverseSpec {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub include: Option<Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub exclude: Vec<String>,
|
pub exclude: Vec<String>,
|
||||||
}
|
}
|
||||||
@@ -1944,6 +1946,27 @@ pub fn platform_expr_config_from_spec(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(universe) = spec.universe.as_ref() {
|
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
|
cfg.universe_exclude = universe
|
||||||
.exclude
|
.exclude
|
||||||
.iter()
|
.iter()
|
||||||
@@ -3009,6 +3032,15 @@ fn normalize_symbol(symbol: &str, raw_board: Option<&str>) -> String {
|
|||||||
instrument_query_id(trimmed, &normalize_board(trimmed, raw_board))
|
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 {
|
fn instrument_query_id(symbol: &str, board: &str) -> String {
|
||||||
if symbol.contains('.') {
|
if symbol.contains('.') {
|
||||||
return symbol.to_ascii_uppercase();
|
return symbol.to_ascii_uppercase();
|
||||||
@@ -3095,7 +3127,10 @@ mod tests {
|
|||||||
"strategyId": "runtime_spec_test",
|
"strategyId": "runtime_spec_test",
|
||||||
"signalSymbol": "000852.SH",
|
"signalSymbol": "000852.SH",
|
||||||
"benchmark": { "instrumentId": "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": {
|
"runtimeExpressions": {
|
||||||
"prelude": "let stocknum = 8;",
|
"prelude": "let stocknum = 8;",
|
||||||
"selection": {
|
"selection": {
|
||||||
@@ -3134,6 +3169,13 @@ mod tests {
|
|||||||
assert_eq!(cfg.signal_symbol, "000852.SH");
|
assert_eq!(cfg.signal_symbol, "000852.SH");
|
||||||
assert_eq!(cfg.selection_limit_expr, "stocknum");
|
assert_eq!(cfg.selection_limit_expr, "stocknum");
|
||||||
assert_eq!(cfg.refresh_rate_expr, "year >= 2024 ? 5 : 20");
|
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_eq!(cfg.universe_exclude, ["paused", "st", "kcb", "one_yuan"]);
|
||||||
assert!(!cfg.rotation_enabled);
|
assert!(!cfg.rotation_enabled);
|
||||||
assert!(cfg.daily_top_up_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]
|
#[test]
|
||||||
fn parses_and_rejects_invalid_position_target_rules() {
|
fn parses_and_rejects_invalid_position_target_rules() {
|
||||||
let spec = serde_json::json!({
|
let spec = serde_json::json!({
|
||||||
|
|||||||
Reference in New Issue
Block a user