修正FIDC风控字段与默认选股语义
This commit is contained in:
@@ -274,7 +274,7 @@ fn band_low(index_close) {
|
|||||||
round((index_close - 2000) * 4 / 500 + 7)
|
round((index_close - 2000) * 4 / 500 + 7)
|
||||||
}"#
|
}"#
|
||||||
.to_string(),
|
.to_string(),
|
||||||
universe_exclude: vec!["bjse".to_string()],
|
universe_exclude: Vec::new(),
|
||||||
market_cap_field: "market_cap".to_string(),
|
market_cap_field: "market_cap".to_string(),
|
||||||
market_cap_lower_expr: "band_low(signal_close)".to_string(),
|
market_cap_lower_expr: "band_low(signal_close)".to_string(),
|
||||||
market_cap_upper_expr: "band_low(signal_close) + 10".to_string(),
|
market_cap_upper_expr: "band_low(signal_close) + 10".to_string(),
|
||||||
@@ -439,7 +439,9 @@ struct StockExpressionState {
|
|||||||
order_step_size: i64,
|
order_step_size: i64,
|
||||||
paused: bool,
|
paused: bool,
|
||||||
is_st: bool,
|
is_st: bool,
|
||||||
|
is_star_st: bool,
|
||||||
is_kcb: bool,
|
is_kcb: bool,
|
||||||
|
is_bjse: bool,
|
||||||
is_one_yuan: bool,
|
is_one_yuan: bool,
|
||||||
is_new_listing: bool,
|
is_new_listing: bool,
|
||||||
allow_buy: bool,
|
allow_buy: bool,
|
||||||
@@ -936,7 +938,9 @@ impl PlatformExprStrategy {
|
|||||||
| "round_lot"
|
| "round_lot"
|
||||||
| "paused"
|
| "paused"
|
||||||
| "is_st"
|
| "is_st"
|
||||||
|
| "is_star_st"
|
||||||
| "is_kcb"
|
| "is_kcb"
|
||||||
|
| "is_bjse"
|
||||||
| "is_one_yuan"
|
| "is_one_yuan"
|
||||||
| "is_new_listing"
|
| "is_new_listing"
|
||||||
| "allow_buy"
|
| "allow_buy"
|
||||||
@@ -2829,7 +2833,9 @@ impl PlatformExprStrategy {
|
|||||||
order_step_size: instrument.map(|item| item.order_step_size()).unwrap_or(100) as i64,
|
order_step_size: instrument.map(|item| item.order_step_size()).unwrap_or(100) as i64,
|
||||||
paused: market.paused || candidate.is_paused,
|
paused: market.paused || candidate.is_paused,
|
||||||
is_st: candidate.is_st,
|
is_st: candidate.is_st,
|
||||||
|
is_star_st: candidate.is_star_st,
|
||||||
is_kcb: candidate.is_kcb,
|
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_one_yuan: candidate.is_one_yuan || market.day_open <= 1.0,
|
||||||
is_new_listing: candidate.is_new_listing,
|
is_new_listing: candidate.is_new_listing,
|
||||||
allow_buy: candidate.allow_buy,
|
allow_buy: candidate.allow_buy,
|
||||||
@@ -3234,7 +3240,9 @@ impl PlatformExprStrategy {
|
|||||||
scope.push("order_step_size", stock.order_step_size);
|
scope.push("order_step_size", stock.order_step_size);
|
||||||
scope.push("paused", stock.paused);
|
scope.push("paused", stock.paused);
|
||||||
scope.push("is_st", stock.is_st);
|
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_kcb", stock.is_kcb);
|
||||||
|
scope.push("is_bjse", stock.is_bjse);
|
||||||
scope.push("is_one_yuan", stock.is_one_yuan);
|
scope.push("is_one_yuan", stock.is_one_yuan);
|
||||||
scope.push("is_new_listing", stock.is_new_listing);
|
scope.push("is_new_listing", stock.is_new_listing);
|
||||||
scope.push("allow_buy", stock.allow_buy);
|
scope.push("allow_buy", stock.allow_buy);
|
||||||
@@ -3344,7 +3352,9 @@ impl PlatformExprStrategy {
|
|||||||
);
|
);
|
||||||
factors.insert("paused".into(), Dynamic::from(stock.paused));
|
factors.insert("paused".into(), Dynamic::from(stock.paused));
|
||||||
factors.insert("is_st".into(), Dynamic::from(stock.is_st));
|
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_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_one_yuan".into(), Dynamic::from(stock.is_one_yuan));
|
||||||
factors.insert("is_new_listing".into(), Dynamic::from(stock.is_new_listing));
|
factors.insert("is_new_listing".into(), Dynamic::from(stock.is_new_listing));
|
||||||
factors.insert("allow_buy".into(), Dynamic::from(stock.allow_buy));
|
factors.insert("allow_buy".into(), Dynamic::from(stock.allow_buy));
|
||||||
@@ -6444,6 +6454,11 @@ impl PlatformExprStrategy {
|
|||||||
None
|
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(
|
fn stock_numeric_field_value(
|
||||||
&self,
|
&self,
|
||||||
candidate: &EligibleUniverseSnapshot,
|
candidate: &EligibleUniverseSnapshot,
|
||||||
@@ -6501,7 +6516,11 @@ impl PlatformExprStrategy {
|
|||||||
}
|
}
|
||||||
"paused" => Some(if stock.paused { 1.0 } else { 0.0 }),
|
"paused" => Some(if stock.paused { 1.0 } else { 0.0 }),
|
||||||
"is_st" => Some(if stock.is_st { 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_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_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 }),
|
"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(
|
"candidate_market_cap" => Some(Self::market_cap_storage_to_strategy_unit(
|
||||||
@@ -7135,7 +7154,9 @@ impl PlatformExprStrategy {
|
|||||||
| "hit_lower_limit"
|
| "hit_lower_limit"
|
||||||
| "paused"
|
| "paused"
|
||||||
| "is_st"
|
| "is_st"
|
||||||
|
| "is_star_st"
|
||||||
| "is_kcb"
|
| "is_kcb"
|
||||||
|
| "is_bjse"
|
||||||
| "is_one_yuan"
|
| "is_one_yuan"
|
||||||
| "is_new_listing"
|
| "is_new_listing"
|
||||||
| "candidate_market_cap"
|
| "candidate_market_cap"
|
||||||
@@ -9095,11 +9116,12 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction,
|
AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction,
|
||||||
DailyFactorSnapshot, DailyMarketSnapshot, DataSet, FactorTextValue, FuturesCommissionType,
|
DailyFactorSnapshot, DailyMarketSnapshot, DataSet, EligibleUniverseSnapshot,
|
||||||
FuturesTradingParameter, Instrument, IntradayExecutionQuote, MatchingType, OpenOrderView,
|
FactorTextValue, FuturesCommissionType, FuturesTradingParameter, Instrument,
|
||||||
OrderIntent, OrderSide, PortfolioState, ProcessEvent, ProcessEventKind, RebalanceCashMode,
|
IntradayExecutionQuote, MatchingType, OpenOrderView, OrderIntent, OrderSide,
|
||||||
ScheduleStage, ScheduleTimeRule, SlippageModel, Strategy, StrategyContext,
|
PortfolioState, ProcessEvent, ProcessEventKind, RebalanceCashMode, ScheduleStage,
|
||||||
TargetPortfolioOrderPricing, TradingCalendar, default_stage_time,
|
ScheduleTimeRule, SlippageModel, Strategy, StrategyContext, TargetPortfolioOrderPricing,
|
||||||
|
TradingCalendar, default_stage_time,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
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]
|
#[test]
|
||||||
fn platform_next_open_select_symbols_defers_decision_day_limit_state() {
|
fn platform_next_open_select_symbols_defers_decision_day_limit_state() {
|
||||||
let decision_date = d(2025, 1, 2);
|
let decision_date = d(2025, 1, 2);
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ const RESERVED_SCOPE_NAMES: &[&str] = &[
|
|||||||
"is_st",
|
"is_st",
|
||||||
"is_star_st",
|
"is_star_st",
|
||||||
"is_kcb",
|
"is_kcb",
|
||||||
|
"is_bjse",
|
||||||
"is_one_yuan",
|
"is_one_yuan",
|
||||||
"is_new_listing",
|
"is_new_listing",
|
||||||
"allow_buy",
|
"allow_buy",
|
||||||
|
|||||||
Reference in New Issue
Block a user