优化回测撮合与涨跌停约束
This commit is contained in:
@@ -111,9 +111,10 @@ impl ChinaAShareRiskControl {
|
||||
if market.paused || candidate.is_paused {
|
||||
return Some("paused");
|
||||
}
|
||||
if !candidate.allow_sell {
|
||||
return Some("sell_disabled");
|
||||
}
|
||||
// `allow_sell` is derived from the daily candidate snapshot and may
|
||||
// reflect an open/close fallback rather than the actual execution tick.
|
||||
// A sell order must be blocked by the execution price lower-limit check
|
||||
// below, while suspension and delisting are handled above.
|
||||
if market.is_at_lower_limit_price(check_price) {
|
||||
return Some("open at or below lower limit");
|
||||
}
|
||||
@@ -134,3 +135,98 @@ impl ChinaAShareRiskControl {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
||||
NaiveDate::from_ymd_opt(year, month, day).expect("valid date")
|
||||
}
|
||||
|
||||
fn candidate(date: NaiveDate) -> CandidateEligibility {
|
||||
CandidateEligibility {
|
||||
date,
|
||||
symbol: "002633.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: false,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn market(date: NaiveDate, last_price: f64, lower_limit: f64) -> DailyMarketSnapshot {
|
||||
DailyMarketSnapshot {
|
||||
date,
|
||||
symbol: "002633.SZ".to_string(),
|
||||
timestamp: Some(format!("{date} 10:18:00")),
|
||||
day_open: last_price,
|
||||
open: last_price,
|
||||
high: last_price,
|
||||
low: last_price,
|
||||
close: last_price,
|
||||
last_price,
|
||||
bid1: last_price,
|
||||
ask1: last_price,
|
||||
prev_close: 6.25,
|
||||
volume: 1_000_000,
|
||||
tick_volume: 10_000,
|
||||
bid1_volume: 10_000,
|
||||
ask1_volume: 10_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 6.89,
|
||||
lower_limit,
|
||||
price_tick: 0.01,
|
||||
}
|
||||
}
|
||||
|
||||
fn position(prev_date: NaiveDate) -> Position {
|
||||
let mut position = Position::new("002633.SZ");
|
||||
position.buy(prev_date, 7_200, 8.48);
|
||||
position
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sell_rejection_uses_execution_price_not_stale_allow_sell() {
|
||||
let prev_date = d(2024, 4, 16);
|
||||
let date = d(2024, 4, 17);
|
||||
let candidate = candidate(date);
|
||||
let market = market(date, 6.27, 5.63);
|
||||
let position = position(prev_date);
|
||||
|
||||
let reason = ChinaAShareRiskControl::sell_rejection_reason(
|
||||
date,
|
||||
&candidate,
|
||||
&market,
|
||||
None,
|
||||
Some(&position),
|
||||
6.27,
|
||||
);
|
||||
|
||||
assert_eq!(reason, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sell_rejection_blocks_execution_price_at_lower_limit() {
|
||||
let prev_date = d(2024, 4, 16);
|
||||
let date = d(2024, 4, 17);
|
||||
let candidate = candidate(date);
|
||||
let market = market(date, 5.63, 5.63);
|
||||
let position = position(prev_date);
|
||||
|
||||
let reason = ChinaAShareRiskControl::sell_rejection_reason(
|
||||
date,
|
||||
&candidate,
|
||||
&market,
|
||||
None,
|
||||
Some(&position),
|
||||
5.63,
|
||||
);
|
||||
|
||||
assert_eq!(reason, Some("open at or below lower limit"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user