669 lines
15 KiB
Rust
669 lines
15 KiB
Rust
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::<Vec<_>>();
|
|
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::<Vec<_>>();
|
|
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="))
|
|
);
|
|
}
|