Files
fidc-backtest-engine/crates/fidc-core/src/rules.rs

109 lines
2.9 KiB
Rust

use chrono::NaiveDate;
use crate::data::{CandidateEligibility, DailyMarketSnapshot, PriceField};
use crate::portfolio::Position;
#[derive(Debug, Clone)]
pub struct RuleCheck {
pub allowed: bool,
pub reason: Option<String>,
}
impl RuleCheck {
pub fn allow() -> Self {
Self {
allowed: true,
reason: None,
}
}
pub fn reject(reason: impl Into<String>) -> Self {
Self {
allowed: false,
reason: Some(reason.into()),
}
}
}
pub trait EquityRuleHooks {
fn can_buy(
&self,
execution_date: NaiveDate,
snapshot: &DailyMarketSnapshot,
candidate: &CandidateEligibility,
price_field: PriceField,
) -> RuleCheck;
fn can_sell(
&self,
execution_date: NaiveDate,
snapshot: &DailyMarketSnapshot,
candidate: &CandidateEligibility,
position: &Position,
price_field: PriceField,
) -> RuleCheck;
}
#[derive(Debug, Clone, Default)]
pub struct ChinaEquityRuleHooks;
impl ChinaEquityRuleHooks {
fn at_upper_limit(snapshot: &DailyMarketSnapshot, price_field: PriceField) -> bool {
snapshot.is_at_upper_limit_price(snapshot.buy_price(price_field))
}
fn at_lower_limit(snapshot: &DailyMarketSnapshot, price_field: PriceField) -> bool {
let check_price = match price_field {
PriceField::Last => snapshot.price(PriceField::Last),
_ => snapshot.sell_price(price_field),
};
snapshot.is_at_lower_limit_price(check_price)
}
}
impl EquityRuleHooks for ChinaEquityRuleHooks {
fn can_buy(
&self,
_execution_date: NaiveDate,
snapshot: &DailyMarketSnapshot,
candidate: &CandidateEligibility,
price_field: PriceField,
) -> RuleCheck {
if snapshot.paused || candidate.is_paused {
return RuleCheck::reject("paused");
}
if !candidate.allow_buy {
return RuleCheck::reject("buy disabled by eligibility flags");
}
if Self::at_upper_limit(snapshot, price_field) {
return RuleCheck::reject("open at or above upper limit");
}
RuleCheck::allow()
}
fn can_sell(
&self,
execution_date: NaiveDate,
snapshot: &DailyMarketSnapshot,
candidate: &CandidateEligibility,
position: &Position,
price_field: PriceField,
) -> RuleCheck {
if snapshot.paused || candidate.is_paused {
return RuleCheck::reject("paused");
}
if !candidate.allow_sell {
return RuleCheck::reject("sell disabled by eligibility flags");
}
if Self::at_lower_limit(snapshot, price_field) {
return RuleCheck::reject("open at or below lower limit");
}
if position.sellable_qty(execution_date) == 0 {
return RuleCheck::reject("t+1 sellable quantity is zero");
}
RuleCheck::allow()
}
}