chore: 更新 fidc-backtest-engine - 2026-05-22
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use crate::data::{CandidateEligibility, DailyMarketSnapshot, PriceField};
|
||||
use crate::instrument::Instrument;
|
||||
use crate::portfolio::Position;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ChinaAShareRiskControl;
|
||||
|
||||
impl ChinaAShareRiskControl {
|
||||
pub fn instrument_rejection_reason(
|
||||
instrument: Option<&Instrument>,
|
||||
date: NaiveDate,
|
||||
) -> Option<&'static str> {
|
||||
let instrument = instrument?;
|
||||
if instrument
|
||||
.listed_at
|
||||
.is_some_and(|listed_at| listed_at > date)
|
||||
{
|
||||
return Some("not_listed");
|
||||
}
|
||||
if instrument
|
||||
.delisted_at
|
||||
.is_some_and(|delisted_at| delisted_at <= date)
|
||||
{
|
||||
return Some("inactive_or_delisted");
|
||||
}
|
||||
let status = instrument.status.trim().to_ascii_lowercase();
|
||||
if matches!(
|
||||
status.as_str(),
|
||||
"inactive" | "delisted" | "terminated" | "expired"
|
||||
) || status.contains("delist")
|
||||
{
|
||||
return Some("inactive_or_delisted");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn selection_rejection_reason(
|
||||
date: NaiveDate,
|
||||
candidate: &CandidateEligibility,
|
||||
market: &DailyMarketSnapshot,
|
||||
instrument: Option<&Instrument>,
|
||||
) -> Option<&'static str> {
|
||||
if let Some(reason) = Self::instrument_rejection_reason(instrument, date) {
|
||||
return Some(reason);
|
||||
}
|
||||
if market.paused || candidate.is_paused {
|
||||
return Some("paused");
|
||||
}
|
||||
if candidate.is_st {
|
||||
return Some("st");
|
||||
}
|
||||
if candidate.is_new_listing {
|
||||
return Some("new_listing");
|
||||
}
|
||||
if candidate.is_kcb {
|
||||
return Some("kcb");
|
||||
}
|
||||
if candidate.is_one_yuan || market.day_open <= 1.0 {
|
||||
return Some("one_yuan");
|
||||
}
|
||||
if !candidate.allow_buy || !candidate.allow_sell {
|
||||
return Some("trade_disabled");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn buy_rejection_reason(
|
||||
date: NaiveDate,
|
||||
candidate: &CandidateEligibility,
|
||||
market: &DailyMarketSnapshot,
|
||||
instrument: Option<&Instrument>,
|
||||
check_price: f64,
|
||||
) -> Option<&'static str> {
|
||||
if let Some(reason) = Self::selection_rejection_reason(date, candidate, market, instrument)
|
||||
{
|
||||
return Some(reason);
|
||||
}
|
||||
if !candidate.allow_buy {
|
||||
return Some("buy_disabled");
|
||||
}
|
||||
if market.is_at_upper_limit_price(check_price) {
|
||||
return Some("open at or above upper limit");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn sell_rejection_reason(
|
||||
date: NaiveDate,
|
||||
candidate: &CandidateEligibility,
|
||||
market: &DailyMarketSnapshot,
|
||||
instrument: Option<&Instrument>,
|
||||
position: Option<&Position>,
|
||||
check_price: f64,
|
||||
) -> Option<&'static str> {
|
||||
if let Some(reason) = Self::instrument_rejection_reason(instrument, date) {
|
||||
return Some(reason);
|
||||
}
|
||||
if market.paused || candidate.is_paused {
|
||||
return Some("paused");
|
||||
}
|
||||
if !candidate.allow_sell {
|
||||
return Some("sell_disabled");
|
||||
}
|
||||
if market.is_at_lower_limit_price(check_price) {
|
||||
return Some("open at or below lower limit");
|
||||
}
|
||||
if position.is_some_and(|position| position.sellable_qty(date) == 0) {
|
||||
return Some("t+1 sellable quantity is zero");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn buy_check_price(market: &DailyMarketSnapshot, price_field: PriceField) -> f64 {
|
||||
market.buy_price(price_field)
|
||||
}
|
||||
|
||||
pub fn sell_check_price(market: &DailyMarketSnapshot, price_field: PriceField) -> f64 {
|
||||
match price_field {
|
||||
PriceField::Last => market.price(PriceField::Last),
|
||||
_ => market.sell_price(price_field),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user