修正FIDC风控字段与默认选股语义
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user