use chrono::NaiveDate; use fidc_core::{ BenchmarkSnapshot, CandidateEligibility, CnSmallCapRotationConfig, CnSmallCapRotationStrategy, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, Instrument, OmniMicroCapConfig, OmniMicroCapStrategy, PortfolioState, Strategy, StrategyContext, }; use std::collections::BTreeSet; fn d(value: &str) -> NaiveDate { NaiveDate::parse_from_str(value, "%Y-%m-%d").unwrap() } fn instrument(symbol: &str, name: &str) -> Instrument { Instrument { symbol: symbol.to_string(), name: name.to_string(), board: "Main".to_string(), round_lot: 100, listed_at: None, delisted_at: None, status: "active".to_string(), } } fn market( date: &str, symbol: &str, open: f64, high: f64, low: f64, close: f64, prev_close: f64, volume: u64, paused: bool, ) -> DailyMarketSnapshot { DailyMarketSnapshot { date: d(date), symbol: symbol.to_string(), timestamp: None, day_open: open, open, high, low, close, last_price: close, bid1: close, ask1: close, prev_close, volume, minute_volume: 0, bid1_volume: 0, ask1_volume: 0, trading_phase: None, paused, upper_limit: (prev_close * 1.10 * 100.0).round() / 100.0, lower_limit: (prev_close * 0.90 * 100.0).round() / 100.0, price_tick: 0.01, } } fn factor( date: &str, symbol: &str, market_cap_bn: f64, free_float_cap_bn: f64, ) -> DailyFactorSnapshot { DailyFactorSnapshot { date: d(date), symbol: symbol.to_string(), market_cap_bn, free_float_cap_bn, pe_ttm: 18.0, turnover_ratio: None, effective_turnover_ratio: None, extra_factors: Default::default(), } } fn candidate( date: &str, symbol: &str, is_new_listing: bool, is_paused: bool, allow_buy: bool, allow_sell: bool, ) -> CandidateEligibility { CandidateEligibility { date: d(date), symbol: symbol.to_string(), is_st: false, is_star_st: false, is_new_listing, is_paused, allow_buy, allow_sell, is_kcb: false, is_one_yuan: false, risk_level_code: None, } } fn benchmark(date: &str, open: f64, close: f64, prev_close: f64, volume: u64) -> BenchmarkSnapshot { BenchmarkSnapshot { date: d(date), benchmark: "CSI300.DEMO".to_string(), open, close, prev_close, volume, } } fn strategy_test_dataset() -> DataSet { let dates = [ "2024-01-02", "2024-01-03", "2024-01-04", "2024-01-05", "2024-01-08", "2024-01-09", "2024-01-10", "2024-01-11", "2024-01-12", ]; let instruments = vec![ instrument("000001.SZ", "Alpha Components"), instrument("000002.SZ", "Beta Precision"), instrument("000003.SZ", "Charlie Materials"), instrument("600001.SH", "Delta Industrials"), ]; let market = vec![ market( "2024-01-02", "000001.SZ", 10.0, 10.2, 9.9, 10.1, 9.8, 1_200_000, false, ), market( "2024-01-02", "000002.SZ", 11.0, 11.3, 10.9, 11.2, 10.8, 1_100_000, false, ), market( "2024-01-02", "000003.SZ", 8.0, 8.1, 7.8, 7.9, 8.0, 900_000, false, ), market( "2024-01-02", "600001.SH", 15.0, 15.2, 14.9, 15.1, 15.0, 800_000, false, ), market( "2024-01-03", "000001.SZ", 10.2, 10.5, 10.1, 10.4, 10.1, 1_250_000, false, ), market( "2024-01-03", "000002.SZ", 11.2, 11.6, 11.1, 11.5, 11.2, 1_120_000, false, ), market( "2024-01-03", "000003.SZ", 7.8, 7.9, 7.3, 7.4, 7.9, 930_000, false, ), market( "2024-01-03", "600001.SH", 15.1, 15.3, 15.0, 15.2, 15.1, 820_000, false, ), market( "2024-01-04", "000001.SZ", 10.5, 10.8, 10.4, 10.7, 10.4, 1_280_000, false, ), market( "2024-01-04", "000002.SZ", 11.4, 11.9, 11.3, 11.8, 11.5, 1_150_000, false, ), market( "2024-01-04", "000003.SZ", 7.3, 7.4, 7.0, 7.1, 7.4, 940_000, false, ), market( "2024-01-04", "600001.SH", 15.2, 15.5, 15.1, 15.4, 15.2, 830_000, false, ), market( "2024-01-05", "000001.SZ", 10.8, 11.1, 10.7, 11.0, 10.7, 1_300_000, false, ), market( "2024-01-05", "000002.SZ", 11.9, 12.1, 11.8, 12.0, 11.8, 1_180_000, false, ), market( "2024-01-05", "000003.SZ", 7.0, 7.1, 6.8, 6.9, 7.1, 950_000, false, ), market( "2024-01-05", "600001.SH", 15.4, 15.6, 15.3, 15.5, 15.4, 840_000, false, ), market( "2024-01-08", "000001.SZ", 11.1, 11.6, 11.0, 11.5, 11.0, 1_400_000, false, ), market( "2024-01-08", "000002.SZ", 12.1, 12.5, 12.0, 12.4, 12.0, 1_200_000, false, ), market( "2024-01-08", "000003.SZ", 7.0, 7.3, 6.9, 7.2, 6.9, 980_000, false, ), market( "2024-01-08", "600001.SH", 15.5, 15.7, 15.4, 15.6, 15.5, 850_000, false, ), market( "2024-01-09", "000001.SZ", 11.6, 12.4, 11.5, 12.3, 11.5, 1_500_000, false, ), market( "2024-01-09", "000002.SZ", 12.5, 12.9, 12.4, 12.8, 12.4, 1_250_000, false, ), market( "2024-01-09", "000003.SZ", 7.2, 7.5, 7.1, 7.4, 7.2, 990_000, false, ), market( "2024-01-09", "600001.SH", 15.6, 15.7, 15.4, 15.5, 15.6, 860_000, false, ), market( "2024-01-10", "000001.SZ", 12.2, 12.3, 11.9, 12.0, 12.3, 1_450_000, false, ), market( "2024-01-10", "000002.SZ", 12.7, 12.8, 12.5, 12.6, 12.8, 1_220_000, false, ), market( "2024-01-10", "000003.SZ", 7.5, 7.6, 7.4, 7.5, 7.4, 1_000_000, false, ), market( "2024-01-10", "600001.SH", 15.4, 15.5, 15.1, 15.2, 15.5, 870_000, false, ), market( "2024-01-11", "000001.SZ", 12.0, 12.1, 11.5, 11.6, 12.0, 1_420_000, false, ), market( "2024-01-11", "000002.SZ", 12.5, 12.6, 12.1, 12.2, 12.6, 1_210_000, false, ), market( "2024-01-11", "000003.SZ", 7.4, 7.5, 7.2, 7.3, 7.5, 980_000, false, ), market( "2024-01-11", "600001.SH", 15.2, 15.2, 15.2, 15.2, 15.2, 0, true, ), market( "2024-01-12", "000001.SZ", 11.5, 11.6, 11.1, 11.2, 11.6, 1_380_000, false, ), market( "2024-01-12", "000002.SZ", 12.1, 12.2, 11.8, 11.9, 12.2, 1_190_000, false, ), market( "2024-01-12", "000003.SZ", 7.2, 7.2, 6.9, 7.0, 7.3, 960_000, false, ), market( "2024-01-12", "600001.SH", 14.8, 15.0, 14.7, 14.9, 15.2, 850_000, false, ), ]; let factors = dates .iter() .enumerate() .flat_map(|(idx, date)| { let i = idx as f64; [ factor(date, "000001.SZ", 38.0 + i, 24.0 + i * 0.5), factor(date, "000002.SZ", 45.0 + i, 30.0 + i * 0.5), factor(date, "000003.SZ", 65.0 - i, 40.0 - i * 0.5), factor(date, "600001.SH", 85.0 + i, 55.0 + i * 0.5), ] }) .collect::>(); let candidates = dates .iter() .flat_map(|date| { let first_two = *date == "2024-01-02" || *date == "2024-01-03"; let paused_600001 = *date == "2024-01-11"; [ candidate(date, "000001.SZ", first_two, false, !first_two, true), candidate(date, "000002.SZ", false, false, true, true), candidate(date, "000003.SZ", false, false, true, true), candidate( date, "600001.SH", false, paused_600001, !paused_600001, !paused_600001, ), ] }) .collect::>(); let benchmarks = vec![ benchmark("2024-01-02", 2990.0, 3000.0, 2980.0, 100_000_000), benchmark("2024-01-03", 3005.0, 3020.0, 3000.0, 102_000_000), benchmark("2024-01-04", 3025.0, 3050.0, 3020.0, 105_000_000), benchmark("2024-01-05", 3055.0, 3080.0, 3050.0, 108_000_000), benchmark("2024-01-08", 3085.0, 3110.0, 3080.0, 109_000_000), benchmark("2024-01-09", 3100.0, 3090.0, 3110.0, 107_000_000), benchmark("2024-01-10", 3080.0, 3040.0, 3090.0, 111_000_000), benchmark("2024-01-11", 3030.0, 2990.0, 3040.0, 115_000_000), benchmark("2024-01-12", 2980.0, 2950.0, 2990.0, 118_000_000), ]; DataSet::from_components(instruments, market, factors, candidates, benchmarks) .expect("strategy test dataset") } #[test] fn strategy_emits_target_weights_and_diagnostics() { let data = strategy_test_dataset(); let decision_date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap(); let execution_date = NaiveDate::from_ymd_opt(2024, 1, 11).unwrap(); let portfolio = PortfolioState::new(1_000_000.0); let mut cfg = CnSmallCapRotationConfig::cn_dyn_smallcap_band(); cfg.signal_symbol = Some("000001.SZ".to_string()); cfg.short_ma_days = 3; cfg.long_ma_days = 5; let mut strategy = CnSmallCapRotationStrategy::new(cfg); let subscriptions = BTreeSet::new(); let decision = strategy .on_day(&StrategyContext { execution_date, decision_date, decision_index: 0, data: &data, portfolio: &portfolio, futures_account: None, open_orders: &[], dynamic_universe: None, subscriptions: &subscriptions, process_events: &[], active_process_event: None, active_datetime: None, order_events: &[], fills: &[], }) .expect("decision"); assert!(decision.rebalance); assert!(decision.rebalance); assert!(!decision.diagnostics.is_empty()); assert!( decision .diagnostics .iter() .any(|line| line.contains("signal_symbol=")) ); assert_eq!(strategy.name(), "cn-dyn-smallcap-band"); } #[test] fn omni_strategy_emits_same_day_decision() { let data = strategy_test_dataset(); let execution_date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap(); let portfolio = PortfolioState::new(1_000_000.0); let mut cfg = OmniMicroCapConfig::omni_microcap(); cfg.benchmark_signal_symbol = "000001.SZ".to_string(); cfg.benchmark_short_ma_days = 3; cfg.benchmark_long_ma_days = 5; cfg.stock_short_ma_days = 3; cfg.stock_mid_ma_days = 4; cfg.stock_long_ma_days = 5; let mut strategy = OmniMicroCapStrategy::new(cfg); let subscriptions = BTreeSet::new(); let decision = strategy .on_day(&StrategyContext { execution_date, decision_date: execution_date, decision_index: 0, data: &data, portfolio: &portfolio, futures_account: None, open_orders: &[], dynamic_universe: None, subscriptions: &subscriptions, process_events: &[], active_process_event: None, active_datetime: None, order_events: &[], fills: &[], }) .expect("omni decision"); assert!(!decision.rebalance); assert!( decision .diagnostics .iter() .any(|line| line.contains("omni_microcap signal=")) ); assert!( decision .diagnostics .iter() .any(|line| line.contains("selected=")) ); }