#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Board {
Main,
ChiNext,
Star,
Beijing,
}
impl Board {
pub fn limit_pct_for(self, st: bool) -> f64 {
match self {
Board::Main if st => 5.0,
Board::Main => 10.0,
Board::ChiNext | Board::Star => 20.0,
Board::Beijing => 30.0,
}
}
pub fn as_str(self) -> &'static str {
match self {
Board::Main => "main",
Board::ChiNext => "chinext",
Board::Star => "star",
Board::Beijing => "beijing",
}
}
}
pub fn board_of(code: &str) -> Option<Board> {
let b = code.as_bytes();
if b.len() != 6 || !b.iter().all(u8::is_ascii_digit) {
return None;
}
match b {
[b'6', b'0', ..] => Some(Board::Main),
[b'6', b'8', b'8' | b'9', ..] => Some(Board::Star),
[b'0', b'0'..=b'3', ..] => Some(Board::Main),
[b'3', b'0', b'0'..=b'2', ..] => Some(Board::ChiNext),
[b'4', b'3', ..] | [b'8', b'2' | b'3' | b'7' | b'8', ..] | [b'9', b'2', ..] => {
Some(Board::Beijing)
}
_ => None,
}
}
pub fn is_st_name(name: &str) -> bool {
name.contains("ST")
}
fn round_fen(price: f64) -> f64 {
(price * 100.0).round() / 100.0
}
fn same_fen(a: f64, b: f64) -> bool {
(a * 100.0).round() == (b * 100.0).round()
}
pub fn limit_up_price(prev_close: f64, board: Board, st: bool) -> Option<f64> {
if !prev_close.is_finite() || prev_close <= 0.0 {
return None;
}
Some(round_fen(
prev_close * (1.0 + board.limit_pct_for(st) / 100.0),
))
}
pub fn limit_down_price(prev_close: f64, board: Board, st: bool) -> Option<f64> {
if !prev_close.is_finite() || prev_close <= 0.0 {
return None;
}
Some(round_fen(
prev_close * (1.0 - board.limit_pct_for(st) / 100.0),
))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LimitStatus {
Sealed,
Blown,
Untouched,
}
impl LimitStatus {
pub fn as_str(self) -> &'static str {
match self {
LimitStatus::Sealed => "sealed",
LimitStatus::Blown => "blown",
LimitStatus::Untouched => "untouched",
}
}
}
pub fn limit_status(
close: f64,
high: f64,
prev_close: f64,
board: Board,
st: bool,
) -> Option<LimitStatus> {
let up = limit_up_price(prev_close, board, st)?;
if same_fen(close, up) {
Some(LimitStatus::Sealed)
} else if same_fen(high, up) || high > up {
Some(LimitStatus::Blown)
} else {
Some(LimitStatus::Untouched)
}
}
pub fn limit_up_streak(statuses: &[LimitStatus]) -> u32 {
statuses
.iter()
.rev()
.take_while(|s| **s == LimitStatus::Sealed)
.count() as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn board_of_by_prefix() {
assert_eq!(board_of("600519"), Some(Board::Main)); assert_eq!(board_of("605111"), Some(Board::Main));
assert_eq!(board_of("000001"), Some(Board::Main)); assert_eq!(board_of("002594"), Some(Board::Main)); assert_eq!(board_of("300750"), Some(Board::ChiNext));
assert_eq!(board_of("301269"), Some(Board::ChiNext));
assert_eq!(board_of("688981"), Some(Board::Star));
assert_eq!(board_of("689009"), Some(Board::Star));
assert_eq!(board_of("832566"), Some(Board::Beijing));
assert_eq!(board_of("430047"), Some(Board::Beijing));
assert_eq!(board_of("920002"), Some(Board::Beijing));
assert_eq!(board_of("510300"), None); assert_eq!(board_of("999999"), None); assert_eq!(board_of("60005"), None); assert_eq!(board_of("000001."), None); assert_eq!(board_of(""), None);
}
#[test]
fn limit_prices_main_10pct() {
let up = limit_up_price(4.26, Board::Main, false).unwrap();
assert_eq!(up, 4.69);
assert_eq!(limit_down_price(4.26, Board::Main, false).unwrap(), 3.83);
}
#[test]
fn limit_prices_by_board_and_st() {
assert_eq!(limit_up_price(10.03, Board::Main, true).unwrap(), 10.53);
assert_eq!(limit_up_price(10.00, Board::ChiNext, false).unwrap(), 12.0);
assert_eq!(limit_up_price(10.00, Board::ChiNext, true).unwrap(), 12.0);
assert_eq!(limit_up_price(50.13, Board::Star, false).unwrap(), 60.16);
assert_eq!(limit_up_price(10.00, Board::Beijing, false).unwrap(), 13.0);
}
#[test]
fn limit_prices_invalid_preclose() {
assert_eq!(limit_up_price(0.0, Board::Main, false), None); assert_eq!(limit_up_price(-1.0, Board::Main, false), None);
assert_eq!(limit_up_price(f64::NAN, Board::Main, false), None);
}
#[test]
fn limit_status_classification() {
let _up = limit_up_price(4.26, Board::Main, false).unwrap(); assert_eq!(
limit_status(4.69, 4.69, 4.26, Board::Main, false),
Some(LimitStatus::Sealed)
);
assert_eq!(
limit_status(4.60, 4.69, 4.26, Board::Main, false),
Some(LimitStatus::Blown)
);
assert_eq!(
limit_status(4.60, 4.65, 4.26, Board::Main, false),
Some(LimitStatus::Untouched)
);
assert_eq!(limit_status(4.69, 4.69, 0.0, Board::Main, false), None);
}
#[test]
fn limit_up_streak_counts_recent_seals() {
let s = [
LimitStatus::Sealed,
LimitStatus::Sealed,
LimitStatus::Blown,
LimitStatus::Sealed,
LimitStatus::Sealed,
LimitStatus::Sealed,
];
assert_eq!(limit_up_streak(&s), 3); assert_eq!(limit_up_streak(&[]), 0);
assert_eq!(limit_up_streak(&[LimitStatus::Blown]), 0);
}
#[test]
fn is_st_name_detection() {
assert!(is_st_name("ST易联众"));
assert!(is_st_name("*ST左江"));
assert!(is_st_name("STO panasonic")); assert!(!is_st_name("平安银行"));
}
#[test]
fn board_as_str_roundtrip_meaning() {
assert_eq!(Board::Main.as_str(), "main");
assert_eq!(Board::ChiNext.as_str(), "chinext");
assert_eq!(Board::Star.as_str(), "star");
assert_eq!(Board::Beijing.as_str(), "beijing");
}
}