109 lines
2.9 KiB
Rust
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()
|
|
}
|
|
}
|