czsc-core 1.0.1

Core analyzer of the czsc framework (most users should depend on the `czsc` facade instead): fractal (FX), stroke (BI), pivot (ZS), CZSC analyzer.
use std::sync::Arc;

use chrono::{TimeZone, Utc};
use czsc_core::analyze::utils::{
    check_gap_info, get_zs_seq, is_bis_down, is_bis_up, is_symmetry_zs,
};
use czsc_core::objects::bar::{NewBar, NewBarBuilder, RawBar, RawBarBuilder};
use czsc_core::objects::bi::{BI, BIBuilder};
use czsc_core::objects::direction::Direction;
use czsc_core::objects::fake_bi::create_fake_bis;
use czsc_core::objects::freq::Freq;
use czsc_core::objects::fx::{FX, FXBuilder};
use czsc_core::objects::mark::Mark;

fn new_bar(ts: i64, high: f64, low: f64) -> NewBar {
    NewBarBuilder::default()
        .symbol(Arc::<str>::from("000001"))
        .dt(Utc.timestamp_opt(ts, 0).unwrap())
        .freq(Freq::F30)
        .id(ts as i32)
        .open((high + low) / 2.0)
        .close((high + low) / 2.0)
        .high(high)
        .low(low)
        .vol(100.0)
        .amount(1000.0)
        .elements(Vec::new())
        .build()
        .unwrap()
}

fn fx(ts: i64, mark: Mark, price: f64) -> FX {
    let (high, low) = match mark {
        Mark::G => (price, price - 1.0),
        Mark::D => (price + 1.0, price),
    };
    FXBuilder::default()
        .symbol(Arc::<str>::from("000001"))
        .dt(Utc.timestamp_opt(ts, 0).unwrap())
        .mark(mark)
        .high(high)
        .low(low)
        .fx(price)
        .elements(vec![
            new_bar(ts - 1, high, low),
            new_bar(ts, high, low),
            new_bar(ts + 1, high, low),
        ])
        .build()
        .unwrap()
}

fn bi(ts: i64, start: f64, end: f64) -> BI {
    let (direction, mark_a, mark_b) = if end > start {
        (Direction::Up, Mark::D, Mark::G)
    } else {
        (Direction::Down, Mark::G, Mark::D)
    };
    let fx_a = fx(ts, mark_a, start);
    let fx_b = fx(ts + 10, mark_b, end);
    BIBuilder::default()
        .symbol(Arc::<str>::from("000001"))
        .fx_a(fx_a.clone())
        .fx_b(fx_b.clone())
        .fxs(vec![fx_a, fx_b])
        .direction(direction)
        .bars(vec![new_bar(ts, start.max(end), start.min(end))])
        .build()
        .unwrap()
}

fn raw_bar(id: i32, high: f64, low: f64) -> RawBar {
    RawBarBuilder::default()
        .symbol(Arc::<str>::from("000001"))
        .dt(Utc.timestamp_opt(id as i64, 0).unwrap())
        .freq(Freq::F30)
        .id(id)
        .open((high + low) / 2.0)
        .close((high + low) / 2.0)
        .high(high)
        .low(low)
        .vol(100.0)
        .amount(1000.0)
        .build()
        .unwrap()
}

#[test]
fn structure_helpers_handle_empty_and_short_inputs() {
    assert!(get_zs_seq(&[]).is_empty());
    assert!(!is_symmetry_zs(&[], 0.3));
    assert!(!is_bis_up(&[]));
    assert!(!is_bis_down(&[]));
    assert!(check_gap_info(&[]).is_empty());
}

#[test]
fn create_fake_bis_builds_expected_up_bi_and_trims_odd_tail() {
    let fxs = vec![
        fx(0, Mark::D, 9.0),
        fx(10, Mark::G, 12.0),
        fx(20, Mark::D, 10.0),
    ];
    let fake_bis = create_fake_bis(&fxs);
    assert_eq!(fake_bis.len(), 1);
    assert_eq!(fake_bis[0].direction, Direction::Up);
    assert_eq!(
        (fake_bis[0].high, fake_bis[0].low, fake_bis[0].power),
        (12.0, 9.0, 3.0)
    );
}

#[test]
#[should_panic(expected = "相邻分型标记必须不同")]
fn create_fake_bis_rejects_non_alternating_fxs() {
    create_fake_bis(&[fx(0, Mark::G, 10.0), fx(10, Mark::G, 11.0)]);
}

#[test]
fn get_zs_seq_groups_overlapping_bis_and_splits_separated_bis() {
    let bis = vec![bi(0, 12.0, 9.0), bi(20, 9.0, 11.0), bi(40, 11.0, 9.5)];
    let grouped = get_zs_seq(&bis);
    assert_eq!(grouped.len(), 1);
    assert_eq!(grouped[0].bis.len(), 3);

    let separated = get_zs_seq(&[bis[0].clone(), bi(20, 5.0, 7.0)]);
    assert_eq!(separated.len(), 2);
}

#[test]
fn symmetry_and_trend_helpers_identify_valid_structures() {
    let symmetric = vec![bi(0, 12.0, 9.0), bi(20, 9.0, 12.0), bi(40, 12.0, 9.0)];
    assert!(is_symmetry_zs(&symmetric, 0.01));

    let up = vec![bi(0, 9.0, 12.0), bi(20, 12.0, 10.0), bi(40, 10.0, 14.0)];
    assert!(is_bis_up(&up));
    assert!(!is_bis_down(&up));

    let down = vec![bi(0, 14.0, 10.0), bi(20, 10.0, 12.0), bi(40, 12.0, 8.0)];
    assert!(is_bis_down(&down));
    assert!(!is_bis_up(&down));
}

#[test]
fn symmetry_and_trend_helpers_reject_invalid_shapes() {
    let asymmetric = vec![bi(0, 12.0, 9.0), bi(20, 9.0, 11.0), bi(40, 11.0, 8.0)];
    assert!(!is_symmetry_zs(&asymmetric, 0.1));
    assert!(!is_symmetry_zs(&asymmetric[..2], 0.3));
    assert!(!is_symmetry_zs(&asymmetric, -0.1));

    let even = vec![bi(0, 9.0, 12.0), bi(20, 12.0, 10.0)];
    assert!(!is_bis_up(&even));
    assert!(!is_bis_down(&even));

    let unordered = vec![bi(40, 9.0, 12.0), bi(0, 12.0, 10.0), bi(20, 10.0, 14.0)];
    assert!(!is_bis_up(&unordered));
}

#[test]
fn check_gap_info_reports_direction_range_and_cover() {
    let bars = vec![
        raw_bar(1, 10.0, 9.0),
        raw_bar(2, 12.0, 11.0),
        raw_bar(3, 11.5, 9.5),
        raw_bar(4, 8.5, 8.0),
    ];
    let gaps = check_gap_info(&bars);
    assert_eq!(gaps.len(), 2);
    assert_eq!(gaps[0].kind, "向上缺口");
    assert_eq!(gaps[0].cover, "已补");
    assert_eq!((gaps[0].high, gaps[0].low), (11.0, 10.0));
    assert_eq!(gaps[1].kind, "向下缺口");
    assert_eq!(gaps[1].cover, "未补");
    assert_eq!((gaps[1].high, gaps[1].low), (9.5, 8.5));
}

#[test]
fn check_gap_info_handles_no_gap_and_covered_down_gap() {
    let no_gap = vec![raw_bar(1, 10.0, 9.0), raw_bar(2, 10.5, 9.5)];
    assert!(check_gap_info(&no_gap).is_empty());

    let down_gap = vec![
        raw_bar(1, 12.0, 11.0),
        raw_bar(2, 10.0, 9.0),
        raw_bar(3, 11.5, 9.5),
    ];
    let gaps = check_gap_info(&down_gap);
    assert_eq!(gaps.len(), 1);
    assert_eq!(gaps[0].kind, "向下缺口");
    assert_eq!(gaps[0].cover, "已补");
    assert_eq!((gaps[0].high, gaps[0].low), (11.0, 10.0));
}