实现FIDC配置化风控与交易成本
This commit is contained in:
@@ -17,7 +17,7 @@ use crate::events::{FillEvent, OrderEvent, OrderSide, OrderStatus, ProcessEvent}
|
||||
use crate::futures::{FuturesAccountState, FuturesOrderIntent};
|
||||
use crate::instrument::Instrument;
|
||||
use crate::portfolio::PortfolioState;
|
||||
use crate::risk_control::ChinaAShareRiskControl;
|
||||
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig, FidcRiskDecisionAudit};
|
||||
use crate::scheduler::ScheduleRule;
|
||||
use crate::universe::{DynamicMarketCapBandSelector, SelectionContext, UniverseSelector};
|
||||
|
||||
@@ -512,6 +512,23 @@ impl StrategyContext<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eligible_universe_on_with_risk_config(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
risk_config: &crate::risk_control::FidcRiskControlConfig,
|
||||
) -> Vec<crate::data::EligibleUniverseSnapshot> {
|
||||
let eligible = self
|
||||
.data
|
||||
.eligible_universe_on_with_risk_config(date, risk_config);
|
||||
match self.dynamic_universe {
|
||||
Some(symbols) if !symbols.is_empty() => eligible
|
||||
.into_iter()
|
||||
.filter(|row| symbols.contains(&row.symbol))
|
||||
.collect(),
|
||||
_ => eligible,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_snapshot(&self, symbol: &str) -> Option<&DailyMarketSnapshot> {
|
||||
self.data.market(self.execution_date, symbol)
|
||||
}
|
||||
@@ -911,6 +928,7 @@ pub struct StrategyDecision {
|
||||
pub order_intents: Vec<OrderIntent>,
|
||||
pub notes: Vec<String>,
|
||||
pub diagnostics: Vec<String>,
|
||||
pub risk_decisions: Vec<FidcRiskDecisionAudit>,
|
||||
}
|
||||
|
||||
impl StrategyDecision {
|
||||
@@ -921,6 +939,7 @@ impl StrategyDecision {
|
||||
self.order_intents.append(&mut other.order_intents);
|
||||
self.notes.append(&mut other.notes);
|
||||
self.diagnostics.append(&mut other.diagnostics);
|
||||
self.risk_decisions.append(&mut other.risk_decisions);
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
@@ -930,6 +949,7 @@ impl StrategyDecision {
|
||||
&& self.order_intents.is_empty()
|
||||
&& self.notes.is_empty()
|
||||
&& self.diagnostics.is_empty()
|
||||
&& self.risk_decisions.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1123,6 +1143,7 @@ pub struct CnSmallCapRotationConfig {
|
||||
pub signal_symbol: Option<String>,
|
||||
pub skip_months: Vec<u32>,
|
||||
pub skip_month_day_ranges: Vec<(Option<u32>, u32, u32, u32)>,
|
||||
pub risk_config: FidcRiskControlConfig,
|
||||
}
|
||||
|
||||
impl CnSmallCapRotationConfig {
|
||||
@@ -1150,6 +1171,7 @@ impl CnSmallCapRotationConfig {
|
||||
signal_symbol: None,
|
||||
skip_months: Vec::new(),
|
||||
skip_month_day_ranges: Vec::new(),
|
||||
risk_config: FidcRiskControlConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1183,6 +1205,7 @@ impl CnSmallCapRotationConfig {
|
||||
(None, 10, 20, 30),
|
||||
(None, 12, 20, 30),
|
||||
],
|
||||
risk_config: FidcRiskControlConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1367,6 +1390,7 @@ impl Strategy for CnSmallCapRotationStrategy {
|
||||
"run_daily(10:17/10:18) mapped to T-1 decision and T open execution"
|
||||
.to_string(),
|
||||
],
|
||||
risk_decisions: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1385,6 +1409,7 @@ impl Strategy for CnSmallCapRotationStrategy {
|
||||
diagnostics: vec![
|
||||
"insufficient history; skip trading on warmup dates".to_string(),
|
||||
],
|
||||
risk_decisions: Vec::new(),
|
||||
});
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
@@ -1422,6 +1447,7 @@ impl Strategy for CnSmallCapRotationStrategy {
|
||||
"run_daily(10:17/10:18) approximated by daily decision/open execution".to_string(),
|
||||
);
|
||||
diagnostics.push("market_cap field mapped from daily_features[_enriched]_v1.market_cap to market_cap_bn without intraday fundamentals refresh".to_string());
|
||||
let mut risk_decisions = Vec::new();
|
||||
|
||||
if rebalance && gross_exposure > 0.0 {
|
||||
let (selected_before_ma, selection_diag) =
|
||||
@@ -1431,6 +1457,7 @@ impl Strategy for CnSmallCapRotationStrategy {
|
||||
reference_level: signal_level,
|
||||
data: ctx.data,
|
||||
dynamic_universe: ctx.dynamic_universe,
|
||||
risk_config: Some(&self.config.risk_config),
|
||||
});
|
||||
let before_ma_count = selected_before_ma.len();
|
||||
let mut ma_rejects = Vec::new();
|
||||
@@ -1479,6 +1506,29 @@ impl Strategy for CnSmallCapRotationStrategy {
|
||||
selection_diag.rejection_examples.join(" | ")
|
||||
));
|
||||
}
|
||||
if !selection_diag.risk_decisions.is_empty() {
|
||||
risk_decisions.extend(selection_diag.risk_decisions.clone());
|
||||
let mut counts = BTreeMap::<String, usize>::new();
|
||||
for decision in &selection_diag.risk_decisions {
|
||||
*counts.entry(decision.rule_code.clone()).or_insert(0) += 1;
|
||||
}
|
||||
diagnostics.push(format!(
|
||||
"risk_decisions selection_total={} by_rule={}",
|
||||
selection_diag.risk_decisions.len(),
|
||||
counts
|
||||
.iter()
|
||||
.map(|(rule, count)| format!("{rule}:{count}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
));
|
||||
diagnostics.extend(
|
||||
selection_diag
|
||||
.risk_decisions
|
||||
.iter()
|
||||
.take(5)
|
||||
.map(|decision| decision.diagnostic_line()),
|
||||
);
|
||||
}
|
||||
if !ma_rejects.is_empty() {
|
||||
diagnostics.push(format!(
|
||||
"ma_filter_rejections sample={}",
|
||||
@@ -1531,6 +1581,7 @@ impl Strategy for CnSmallCapRotationStrategy {
|
||||
order_intents: Vec::new(),
|
||||
notes,
|
||||
diagnostics,
|
||||
risk_decisions,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1560,6 +1611,7 @@ pub struct OmniMicroCapConfig {
|
||||
pub stop_loss_ratio: f64,
|
||||
pub take_profit_ratio: f64,
|
||||
pub skip_month_day_ranges: Vec<(Option<u32>, u32, u32, u32)>,
|
||||
pub risk_config: FidcRiskControlConfig,
|
||||
}
|
||||
|
||||
impl OmniMicroCapConfig {
|
||||
@@ -1590,6 +1642,7 @@ impl OmniMicroCapConfig {
|
||||
// The migrated reference logic disables seasonal stop windows in
|
||||
// production-style execution, so the default keeps that behavior.
|
||||
skip_month_day_ranges: Vec::new(),
|
||||
risk_config: FidcRiskControlConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1618,6 +1671,7 @@ impl OmniMicroCapConfig {
|
||||
stop_loss_ratio: 0.92,
|
||||
take_profit_ratio: 1.16,
|
||||
skip_month_day_ranges: Vec::new(),
|
||||
risk_config: FidcRiskControlConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1687,12 +1741,16 @@ impl OmniMicroCapStrategy {
|
||||
0.0
|
||||
}
|
||||
|
||||
fn cost_model(&self) -> ChinaAShareCostModel {
|
||||
ChinaAShareCostModel::from_trading_constraints(self.config.risk_config.trading_constraints)
|
||||
}
|
||||
|
||||
fn buy_commission(&self, gross_amount: f64) -> f64 {
|
||||
ChinaAShareCostModel::default().commission_for(gross_amount)
|
||||
self.cost_model().commission_for(gross_amount)
|
||||
}
|
||||
|
||||
fn sell_cost(&self, date: NaiveDate, gross_amount: f64) -> f64 {
|
||||
let model = ChinaAShareCostModel::default();
|
||||
let model = self.cost_model();
|
||||
model.commission_for(gross_amount)
|
||||
+ model.stamp_tax_for(date, OrderSide::Sell, gross_amount)
|
||||
}
|
||||
@@ -1974,36 +2032,47 @@ impl OmniMicroCapStrategy {
|
||||
}
|
||||
|
||||
let mut max_fill = requested_qty;
|
||||
let top_level_liquidity = match side {
|
||||
OrderSide::Buy => snapshot.liquidity_for_buy(),
|
||||
OrderSide::Sell => snapshot.liquidity_for_sell(),
|
||||
let constraints = self.config.risk_config.trading_constraints;
|
||||
if constraints.liquidity_limit_enabled {
|
||||
let top_level_liquidity = match side {
|
||||
OrderSide::Buy => snapshot.liquidity_for_buy(),
|
||||
OrderSide::Sell => snapshot.liquidity_for_sell(),
|
||||
}
|
||||
.min(u32::MAX as u64) as u32;
|
||||
if top_level_liquidity == 0 {
|
||||
return None;
|
||||
}
|
||||
let liquidity_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
|
||||
top_level_liquidity
|
||||
} else {
|
||||
self.round_lot_quantity(
|
||||
top_level_liquidity,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
)
|
||||
};
|
||||
max_fill = max_fill.min(liquidity_limited);
|
||||
}
|
||||
.min(u32::MAX as u64) as u32;
|
||||
if top_level_liquidity == 0 {
|
||||
return None;
|
||||
}
|
||||
let liquidity_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
|
||||
top_level_liquidity
|
||||
} else {
|
||||
self.round_lot_quantity(top_level_liquidity, minimum_order_quantity, order_step_size)
|
||||
};
|
||||
max_fill = max_fill.min(liquidity_limited);
|
||||
|
||||
let consumed_turnover = *execution_state.intraday_turnover.get(symbol).unwrap_or(&0);
|
||||
let raw_limit =
|
||||
((snapshot.minute_volume as f64) * 0.25).round() as i64 - consumed_turnover as i64;
|
||||
if raw_limit <= 0 {
|
||||
return None;
|
||||
if constraints.volume_limit_enabled {
|
||||
let raw_limit = ((snapshot.minute_volume as f64) * constraints.volume_percent).round()
|
||||
as i64
|
||||
- consumed_turnover as i64;
|
||||
if raw_limit <= 0 {
|
||||
return None;
|
||||
}
|
||||
let volume_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
|
||||
raw_limit as u32
|
||||
} else {
|
||||
self.round_lot_quantity(raw_limit as u32, minimum_order_quantity, order_step_size)
|
||||
};
|
||||
if volume_limited == 0 {
|
||||
return None;
|
||||
}
|
||||
max_fill = max_fill.min(volume_limited);
|
||||
}
|
||||
let volume_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
|
||||
raw_limit as u32
|
||||
} else {
|
||||
self.round_lot_quantity(raw_limit as u32, minimum_order_quantity, order_step_size)
|
||||
};
|
||||
if volume_limited == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(max_fill.min(volume_limited))
|
||||
Some(max_fill)
|
||||
}
|
||||
|
||||
fn projected_execution_start_cursor(
|
||||
@@ -2338,13 +2407,14 @@ impl OmniMicroCapStrategy {
|
||||
let Ok(candidate) = ctx.data.require_candidate(date, symbol) else {
|
||||
return false;
|
||||
};
|
||||
ChinaAShareRiskControl::sell_rejection_reason(
|
||||
ChinaAShareRiskControl::sell_rejection_reason_with_config(
|
||||
date,
|
||||
candidate,
|
||||
market,
|
||||
ctx.data.instrument(symbol),
|
||||
Some(position),
|
||||
ChinaAShareRiskControl::sell_check_price(market, PriceField::Last),
|
||||
&self.config.risk_config,
|
||||
)
|
||||
.is_none()
|
||||
}
|
||||
@@ -2358,12 +2428,13 @@ impl OmniMicroCapStrategy {
|
||||
let market = ctx.data.require_market(date, symbol)?;
|
||||
let candidate = ctx.data.require_candidate(date, symbol)?;
|
||||
|
||||
if let Some(reason) = ChinaAShareRiskControl::buy_rejection_reason(
|
||||
if let Some(reason) = ChinaAShareRiskControl::buy_rejection_reason_with_config(
|
||||
date,
|
||||
candidate,
|
||||
market,
|
||||
ctx.data.instrument(symbol),
|
||||
ChinaAShareRiskControl::buy_check_price(market, PriceField::Last),
|
||||
&self.config.risk_config,
|
||||
) {
|
||||
return Ok(Some(reason.to_string()));
|
||||
}
|
||||
@@ -2375,6 +2446,64 @@ impl OmniMicroCapStrategy {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn selection_risk_decisions(
|
||||
&self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
date: NaiveDate,
|
||||
) -> Vec<FidcRiskDecisionAudit> {
|
||||
let mut decisions = Vec::new();
|
||||
for factor in ctx.data.factor_snapshots_on(date) {
|
||||
if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) {
|
||||
continue;
|
||||
}
|
||||
let Some(candidate) = ctx.data.candidate(date, &factor.symbol) else {
|
||||
continue;
|
||||
};
|
||||
let Some(market) = ctx.data.market(date, &factor.symbol) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(decision) = ChinaAShareRiskControl::selection_rejection_decision_with_config(
|
||||
date,
|
||||
candidate,
|
||||
market,
|
||||
ctx.data.instrument(&factor.symbol),
|
||||
&self.config.risk_config,
|
||||
) {
|
||||
decisions.push(decision);
|
||||
}
|
||||
}
|
||||
decisions
|
||||
}
|
||||
|
||||
fn selection_risk_decision_diagnostics(
|
||||
decisions: &[FidcRiskDecisionAudit],
|
||||
sample_limit: usize,
|
||||
) -> Vec<String> {
|
||||
if decisions.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut counts = BTreeMap::<String, usize>::new();
|
||||
for decision in decisions {
|
||||
*counts.entry(decision.rule_code.clone()).or_insert(0) += 1;
|
||||
}
|
||||
let mut diagnostics = vec![format!(
|
||||
"risk_decisions selection_total={} by_rule={}",
|
||||
decisions.len(),
|
||||
counts
|
||||
.iter()
|
||||
.map(|(rule, count)| format!("{rule}:{count}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)];
|
||||
diagnostics.extend(
|
||||
decisions
|
||||
.iter()
|
||||
.take(sample_limit)
|
||||
.map(|decision| decision.diagnostic_line()),
|
||||
);
|
||||
diagnostics
|
||||
}
|
||||
|
||||
fn select_symbols(
|
||||
&self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
@@ -2418,7 +2547,8 @@ impl OmniMicroCapStrategy {
|
||||
}
|
||||
|
||||
if selected.len() < self.config.stocknum {
|
||||
let universe = ctx.eligible_universe_on(date);
|
||||
let universe =
|
||||
ctx.eligible_universe_on_with_risk_config(date, &self.config.risk_config);
|
||||
let start = lower_bound_eligible(&universe, band_low);
|
||||
for candidate in universe.iter().skip(start) {
|
||||
if candidate.market_cap_bn > band_high {
|
||||
@@ -2453,7 +2583,7 @@ impl OmniMicroCapStrategy {
|
||||
return Ok((selected, diagnostics));
|
||||
}
|
||||
|
||||
let universe = ctx.eligible_universe_on(date);
|
||||
let universe = ctx.eligible_universe_on_with_risk_config(date, &self.config.risk_config);
|
||||
let mut diagnostics = Vec::new();
|
||||
let mut selected = Vec::new();
|
||||
let start = lower_bound_eligible(&universe, band_low);
|
||||
@@ -2654,6 +2784,7 @@ impl Strategy for OmniMicroCapStrategy {
|
||||
.collect(),
|
||||
notes: vec![format!("seasonal stop window on {}", date)],
|
||||
diagnostics: vec!["platform-native skip window forced all cash".to_string()],
|
||||
risk_decisions: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2672,6 +2803,7 @@ impl Strategy for OmniMicroCapStrategy {
|
||||
diagnostics: vec![
|
||||
"insufficient history; skip trading on warmup dates".to_string(),
|
||||
],
|
||||
risk_decisions: Vec::new(),
|
||||
});
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
@@ -2679,6 +2811,7 @@ impl Strategy for OmniMicroCapStrategy {
|
||||
// 使用前一交易日的指数价格计算市值区间(模拟实盘场景)
|
||||
let (band_low, band_high) = self.market_cap_band(prev_index_level);
|
||||
let (stock_list, selection_notes) = self.select_symbols(ctx, date, band_low, band_high)?;
|
||||
let risk_decisions = self.selection_risk_decisions(ctx, date);
|
||||
let periodic_rebalance = ctx.decision_index % self.config.refresh_rate == 0;
|
||||
let mut projected = ctx.portfolio.clone();
|
||||
let mut projected_execution_state = ProjectedExecutionState::default();
|
||||
@@ -2848,6 +2981,10 @@ impl Strategy for OmniMicroCapStrategy {
|
||||
));
|
||||
}
|
||||
diagnostics.extend(selection_notes);
|
||||
diagnostics.extend(Self::selection_risk_decision_diagnostics(
|
||||
&risk_decisions,
|
||||
5,
|
||||
));
|
||||
|
||||
let notes = vec![
|
||||
format!("stock_list={}", stock_list.len()),
|
||||
@@ -2861,6 +2998,7 @@ impl Strategy for OmniMicroCapStrategy {
|
||||
order_intents,
|
||||
notes,
|
||||
diagnostics,
|
||||
risk_decisions,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2882,6 +3020,7 @@ fn lower_bound_eligible(rows: &[crate::data::EligibleUniverseSnapshot], target:
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{BenchmarkSnapshot, CandidateEligibility, DailyFactorSnapshot};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn temp_csv_path(name: &str) -> PathBuf {
|
||||
@@ -2927,4 +3066,168 @@ mod tests {
|
||||
Some("603657.SH")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omni_microcap_projection_uses_configured_trading_cost() {
|
||||
let mut cfg = OmniMicroCapConfig::omni_microcap();
|
||||
cfg.risk_config.trading_constraints.commission_rate = 0.0003;
|
||||
cfg.risk_config.trading_constraints.minimum_commission = 5.0;
|
||||
cfg.risk_config
|
||||
.trading_constraints
|
||||
.stamp_tax_rate_after_change = 0.0005;
|
||||
let strategy = OmniMicroCapStrategy::new(cfg);
|
||||
|
||||
assert!((strategy.buy_commission(100_000.0) - 30.0).abs() < 1e-9);
|
||||
assert!((strategy.buy_commission(1_000.0) - 5.0).abs() < 1e-9);
|
||||
assert!(
|
||||
(strategy.sell_cost(NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(), 100_000.0) - 80.0)
|
||||
.abs()
|
||||
< 1e-9
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omni_microcap_selection_uses_configured_risk_policy() {
|
||||
let dates = [
|
||||
NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2025, 1, 3).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2025, 1, 6).unwrap(),
|
||||
];
|
||||
let symbol = "688001.SH";
|
||||
let market_rows = dates
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, date)| DailyMarketSnapshot {
|
||||
date: *date,
|
||||
symbol: symbol.to_string(),
|
||||
timestamp: Some(format!("{date} 10:18:00")),
|
||||
day_open: 10.0 + index as f64,
|
||||
open: 10.0 + index as f64,
|
||||
high: 10.4 + index as f64,
|
||||
low: 9.8 + index as f64,
|
||||
close: 10.2 + index as f64,
|
||||
last_price: 10.2 + index as f64,
|
||||
bid1: 10.1 + index as f64,
|
||||
ask1: 10.2 + index as f64,
|
||||
prev_close: 10.0 + index as f64,
|
||||
volume: 1_000_000 + index as u64 * 100_000,
|
||||
minute_volume: 10_000,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 20.0,
|
||||
lower_limit: 5.0,
|
||||
price_tick: 0.01,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let factor_rows = dates
|
||||
.iter()
|
||||
.map(|date| DailyFactorSnapshot {
|
||||
date: *date,
|
||||
symbol: symbol.to_string(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 9.0,
|
||||
pe_ttm: 12.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
extra_factors: BTreeMap::new(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let candidate_rows = dates
|
||||
.iter()
|
||||
.map(|date| CandidateEligibility {
|
||||
date: *date,
|
||||
symbol: symbol.to_string(),
|
||||
is_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: true,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let benchmark_rows = dates
|
||||
.iter()
|
||||
.map(|date| BenchmarkSnapshot {
|
||||
date: *date,
|
||||
benchmark: "000852.SH".to_string(),
|
||||
open: 100.0,
|
||||
close: 101.0,
|
||||
prev_close: 99.0,
|
||||
volume: 1_000_000,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let data = DataSet::from_components(
|
||||
vec![Instrument {
|
||||
symbol: symbol.to_string(),
|
||||
name: symbol.to_string(),
|
||||
board: "SH".to_string(),
|
||||
round_lot: 100,
|
||||
listed_at: Some(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()),
|
||||
delisted_at: None,
|
||||
status: "active".to_string(),
|
||||
}],
|
||||
market_rows,
|
||||
factor_rows,
|
||||
candidate_rows,
|
||||
benchmark_rows,
|
||||
)
|
||||
.expect("dataset");
|
||||
let portfolio = PortfolioState::new(1_000_000.0);
|
||||
let subscriptions = BTreeSet::new();
|
||||
let ctx = StrategyContext {
|
||||
execution_date: dates[2],
|
||||
decision_date: dates[2],
|
||||
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 default_cfg = OmniMicroCapConfig::omni_microcap();
|
||||
default_cfg.strategy_name = "configured_risk_policy_test".to_string();
|
||||
default_cfg.stock_short_ma_days = 1;
|
||||
default_cfg.stock_mid_ma_days = 2;
|
||||
default_cfg.stock_long_ma_days = 3;
|
||||
let default_strategy = OmniMicroCapStrategy::new(default_cfg.clone());
|
||||
let (default_selected, _) = default_strategy
|
||||
.select_symbols(&ctx, dates[2], 0.0, 100.0)
|
||||
.expect("default selection");
|
||||
assert!(default_selected.is_empty());
|
||||
let default_risk_decisions = default_strategy.selection_risk_decisions(&ctx, dates[2]);
|
||||
assert_eq!(default_risk_decisions.len(), 1);
|
||||
assert_eq!(default_risk_decisions[0].symbol, symbol);
|
||||
assert_eq!(default_risk_decisions[0].rule_code, "kcb");
|
||||
assert!(
|
||||
default_risk_decisions[0]
|
||||
.diagnostic_line()
|
||||
.starts_with("risk_decision=")
|
||||
);
|
||||
|
||||
let mut cfg = default_cfg;
|
||||
cfg.risk_config.static_rules.reject_kcb_selection = false;
|
||||
cfg.risk_config.static_rules.reject_kcb_buy = false;
|
||||
let configured_strategy = OmniMicroCapStrategy::new(cfg);
|
||||
let (selected, _) = configured_strategy
|
||||
.select_symbols(&ctx, dates[2], 0.0, 100.0)
|
||||
.expect("configured selection");
|
||||
assert!(
|
||||
configured_strategy
|
||||
.selection_risk_decisions(&ctx, dates[2])
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
assert_eq!(selected, vec![symbol.to_string()]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user