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, } impl RuleCheck { pub fn allow() -> Self { Self { allowed: true, reason: None, } } pub fn reject(reason: impl Into) -> 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() } }