增强回测引擎第二版策略与快照层

This commit is contained in:
zsb
2026-04-07 00:34:52 -07:00
parent 334864cbc5
commit d039c4e741
10 changed files with 244 additions and 86 deletions

View File

@@ -4,6 +4,7 @@
## 当前能力
- Phase 2增加 snapshot bundle 视图与更贴近 jqdata 策略语义的动态市值带策略
- 日频交易日历与确定性逐日回放
- A 股日频市场快照、估值/因子快照、基准快照、候选资格标记
- 策略接口与引擎驱动,不直接模拟 `jqdata` API
@@ -53,19 +54,24 @@
## 策略实现
示例策略 `CnSmallCapRotationStrategy` 对应一类典型的 A 股小市值轮动逻辑:
示例策略 `CnSmallCapRotationStrategy` 对应一类典型的 A 股小市值轮动逻辑,并在 phase 2 里更贴近原始 jqdata 语义
1. 用指数点位相对基准水平切换市值带:
- 强势区间:更偏小市值
- 中性区间:中小市值
- 弱势区间:偏大一些的防御市值带
1. 用指数点位动态计算市值带:
- `mystart = round((index_close - base_index_level) * xs + base_cap_floor)`
- `myend = mystart + cap_span`
2. 在当前市值带内,按总市值升序取 Top-N。
3. 用指数短均线/长均线关系控制总仓位:
- `1.0`: 风险偏好正常
- `0.5`: 降半仓
- `0.0`: 全部转现金
4. 固定交易日频率再平衡。
- `MA(short) < MA(long) * rsi_rate` 时降到 `trade_rate`
- 否则恢复到 `1.0`
4.`refresh_rate` 固定频率再平衡。
5. 非再平衡日也会检查止损/止盈钩子并触发退出。
6. 候选过滤纳入资格快照:
- 停牌
- ST
- 新股
- 科创板
- 1 元股
- allow_buy / allow_sell
这个接口不是 `jqdata` 风格的 `before_trading_start` / `handle_data` 直接脚本 API而是
@@ -79,7 +85,7 @@
如果原始逻辑大致是:
- 依据指数强弱切换可接受市值带
- 依据指数点位动态切换可接受市值带
- 从候选股票里选最小市值若干只
- 按均线决定是否降仓
- 周期性调仓
@@ -94,7 +100,14 @@
- `order_target_value` -> `StrategyDecision.target_weights``BrokerSimulator` 解释执行
- 风险控制逻辑 -> `CnSmallCapRotationStrategy::gross_exposure`
## V1 明确简化点
## Phase 2 新增内容
- `DataSet::bundle_on(date)`:引入按日 snapshot bundle 视图,方便未来直接对接 FiDataCenter / FiDataScraper 预计算快照
- 策略诊断输出equity curve 里新增 `diagnostics` 字段,记录市值带、候选样本、退出原因等信息
- 候选资格快照扩展:补入 `is_kcb``is_one_yuan`
- 增加策略选择行为测试
## V1 / V2 当前仍保留的简化点
下面这些是刻意保留为 v1 简化,而不是遗漏:
@@ -146,6 +159,16 @@ cargo build
- 组合调仓只关心“目标持仓”和“当前持仓”的差量
- 事件流是 append-only适合批量写出和后处理分析
## 距离真实 6 年 / 5 分钟平台还差什么
当前仓库已经有“核心引擎 + 规则钩子 + 策略接口 + demo 回放”,但距离生产级目标还差:
- 真实 snapshot loader接入 FiDataCenter / FiDataScraper 的 ClickHouse / Parquet / PostgreSQL 预计算快照,而不是 demo CSV
- 分钟级执行层:把当前 `T-1 决策 / T 开盘执行` 扩展到更接近 `10:17 / 10:18` 的分钟级执行语义
- 更完整的 A 股规则:复权、分红、涨跌停细分、创业板/北交所规则、成交量约束、滑点模型
- 更高效的数据访问:按日期块和列式布局一次性加载 6 年快照,避免回测时回源拼表
- 批量参数回测:多个参数集共享预计算快照与候选池缓存
## Roadmap
- 引入更明确的事件总线和 portfolio/account ledger 分层

View File

@@ -26,7 +26,11 @@ fn main() -> Result<(), Box<dyn Error>> {
fs::create_dir_all(&output_dir)?;
let data = DataSet::from_csv_dir(&data_dir)?;
let strategy = CnSmallCapRotationStrategy::new(CnSmallCapRotationConfig::demo());
let mut strategy_cfg = CnSmallCapRotationConfig::demo();
strategy_cfg.base_index_level = 3000.0;
strategy_cfg.base_cap_floor = 38.0;
strategy_cfg.cap_span = 25.0;
let strategy = CnSmallCapRotationStrategy::new(strategy_cfg);
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default());
let config = BacktestConfig {
initial_cash: 1_000_000.0,
@@ -60,17 +64,18 @@ fn workspace_root() -> PathBuf {
fn write_equity_curve_csv(path: &Path, rows: &[DailyEquityPoint]) -> Result<(), Box<dyn Error>> {
let mut file = fs::File::create(path)?;
writeln!(file, "date,cash,market_value,total_equity,benchmark_close,notes")?;
writeln!(file, "date,cash,market_value,total_equity,benchmark_close,notes,diagnostics")?;
for row in rows {
writeln!(
file,
"{},{:.2},{:.2},{:.2},{:.2},{}",
"{},{:.2},{:.2},{:.2},{:.2},{},{}",
row.date,
row.cash,
row.market_value,
row.total_equity,
row.benchmark_close,
sanitize_csv_field(&row.notes),
sanitize_csv_field(&row.diagnostics),
)?;
}
Ok(())

View File

@@ -110,14 +110,31 @@ pub struct CandidateEligibility {
pub is_paused: bool,
pub allow_buy: bool,
pub allow_sell: bool,
pub is_kcb: bool,
pub is_one_yuan: bool,
}
impl CandidateEligibility {
pub fn eligible_for_selection(&self) -> bool {
!self.is_st && !self.is_new_listing && !self.is_paused && self.allow_buy && self.allow_sell
!self.is_st
&& !self.is_new_listing
&& !self.is_paused
&& !self.is_kcb
&& !self.is_one_yuan
&& self.allow_buy
&& self.allow_sell
}
}
#[derive(Debug, Clone)]
pub struct DailySnapshotBundle {
pub date: NaiveDate,
pub benchmark: BenchmarkSnapshot,
pub market: Vec<DailyMarketSnapshot>,
pub factors: Vec<DailyFactorSnapshot>,
pub candidates: Vec<CandidateEligibility>,
}
#[derive(Debug, Clone)]
pub struct DataSet {
instruments: HashMap<String, Instrument>,
@@ -246,6 +263,20 @@ impl DataSet {
.unwrap_or_default()
}
pub fn bundle_on(&self, date: NaiveDate) -> Result<DailySnapshotBundle, DataSetError> {
let benchmark = self
.benchmark(date)
.cloned()
.ok_or(DataSetError::MissingBenchmark { date })?;
Ok(DailySnapshotBundle {
date,
benchmark,
market: self.market_by_date.get(&date).cloned().unwrap_or_default(),
factors: self.factor_by_date.get(&date).cloned().unwrap_or_default(),
candidates: self.candidate_by_date.get(&date).cloned().unwrap_or_default(),
})
}
pub fn benchmark_closes_up_to(&self, date: NaiveDate, lookback: usize) -> Vec<f64> {
self.calendar
.trailing_days(date, lookback)
@@ -342,6 +373,8 @@ fn read_candidates(path: &Path) -> Result<Vec<CandidateEligibility>, DataSetErro
is_paused: row.parse_bool(4)?,
allow_buy: row.parse_bool(5)?,
allow_sell: row.parse_bool(6)?,
is_kcb: row.parse_optional_bool(7).unwrap_or(false),
is_one_yuan: row.parse_optional_bool(8).unwrap_or(false),
});
}
Ok(snapshots)
@@ -415,6 +448,12 @@ impl CsvRow {
message: format!("invalid bool: {err}"),
})
}
fn parse_optional_bool(&self, index: usize) -> Option<bool> {
self.fields
.get(index)
.and_then(|value| value.parse::<bool>().ok())
}
}
fn read_rows(path: &Path) -> Result<Vec<CsvRow>, DataSetError> {

View File

@@ -41,6 +41,7 @@ pub struct DailyEquityPoint {
pub total_equity: f64,
pub benchmark_close: f64,
pub notes: String,
pub diagnostics: String,
}
#[derive(Debug, Clone)]
@@ -126,6 +127,7 @@ where
date: execution_date,
})?;
let notes = decision.notes.join(" | ");
let diagnostics = decision.diagnostics.join(" | ");
result.equity_curve.push(DailyEquityPoint {
date: execution_date,
@@ -134,6 +136,7 @@ where
total_equity: portfolio.total_equity(),
benchmark_close: benchmark.close,
notes,
diagnostics,
});
}

View File

@@ -18,6 +18,7 @@ pub use data::{
CandidateEligibility,
DailyFactorSnapshot,
DailyMarketSnapshot,
DailySnapshotBundle,
DataSet,
DataSetError,
PriceField,

View File

@@ -26,14 +26,21 @@ pub struct StrategyDecision {
pub target_weights: BTreeMap<String, f64>,
pub exit_symbols: BTreeSet<String>,
pub notes: Vec<String>,
pub diagnostics: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CnSmallCapRotationConfig {
pub rebalance_every_n_days: usize,
pub max_positions: usize,
pub refresh_rate: usize,
pub stocknum: usize,
pub xs: f64,
pub base_index_level: f64,
pub base_cap_floor: f64,
pub cap_span: f64,
pub short_ma_days: usize,
pub long_ma_days: usize,
pub rsi_rate: f64,
pub trade_rate: f64,
pub stop_loss_pct: f64,
pub take_profit_pct: f64,
}
@@ -41,10 +48,16 @@ pub struct CnSmallCapRotationConfig {
impl CnSmallCapRotationConfig {
pub fn demo() -> Self {
Self {
rebalance_every_n_days: 3,
max_positions: 2,
refresh_rate: 3,
stocknum: 2,
xs: 4.0 / 500.0,
base_index_level: 2000.0,
base_cap_floor: 7.0,
cap_span: 10.0,
short_ma_days: 3,
long_ma_days: 5,
rsi_rate: 1.0001,
trade_rate: 0.5,
stop_loss_pct: 0.08,
take_profit_pct: 0.10,
}
@@ -60,7 +73,13 @@ pub struct CnSmallCapRotationStrategy {
impl CnSmallCapRotationStrategy {
pub fn new(config: CnSmallCapRotationConfig) -> Self {
Self {
selector: DynamicMarketCapBandSelector::demo(config.max_positions),
selector: DynamicMarketCapBandSelector::new(
config.base_index_level,
config.base_cap_floor,
config.cap_span,
config.xs,
config.stocknum,
),
config,
last_gross_exposure: None,
}
@@ -86,12 +105,12 @@ impl CnSmallCapRotationStrategy {
let short_ma = Self::moving_average(closes, self.config.short_ma_days);
let long_ma = Self::moving_average(closes, self.config.long_ma_days);
if current >= long_ma && short_ma >= long_ma {
if short_ma < long_ma * self.config.rsi_rate {
self.config.trade_rate
} else if current >= long_ma {
1.0
} else if current >= long_ma || short_ma >= long_ma {
0.5
} else {
0.0
self.config.trade_rate
}
}
@@ -142,7 +161,7 @@ impl Strategy for CnSmallCapRotationStrategy {
.data
.benchmark_closes_up_to(ctx.decision_date, self.config.long_ma_days);
let gross_exposure = self.gross_exposure(&benchmark_closes);
let periodic_rebalance = ctx.decision_index % self.config.rebalance_every_n_days == 0;
let periodic_rebalance = ctx.decision_index % self.config.refresh_rate == 0;
let exposure_changed = self
.last_gross_exposure
.map(|previous| (previous - gross_exposure).abs() > f64::EPSILON)
@@ -155,6 +174,14 @@ impl Strategy for CnSmallCapRotationStrategy {
"decision={} exec={} exposure={:.2}",
ctx.decision_date, ctx.execution_date, gross_exposure
)];
let mut diagnostics = vec![format!(
"benchmark_close={:.2} refresh_rate={} stocknum={} short_ma_days={} long_ma_days={}",
benchmark.close,
self.config.refresh_rate,
self.config.stocknum,
self.config.short_ma_days,
self.config.long_ma_days,
)];
if rebalance && gross_exposure > 0.0 {
let selected = self.selector.select(&SelectionContext {
@@ -165,9 +192,21 @@ impl Strategy for CnSmallCapRotationStrategy {
if !selected.is_empty() {
let per_name_weight = gross_exposure / selected.len() as f64;
for candidate in selected {
for candidate in &selected {
target_weights.insert(candidate.symbol.clone(), per_name_weight);
}
diagnostics.push(format!(
"selected={} cap_band={:.2}-{:.2} sample={}",
selected.len(),
selected.first().map(|x| x.band_low).unwrap_or_default(),
selected.first().map(|x| x.band_high).unwrap_or_default(),
selected
.iter()
.take(5)
.map(|x| format!("{}:{:.2}", x.symbol, x.market_cap_bn))
.collect::<Vec<_>>()
.join("|")
));
}
notes.push(format!("rebalance names={}", target_weights.len()));
@@ -175,6 +214,10 @@ impl Strategy for CnSmallCapRotationStrategy {
if !exit_symbols.is_empty() {
notes.push(format!("exit hooks={}", exit_symbols.len()));
diagnostics.push(format!(
"exit_symbols={}",
exit_symbols.iter().cloned().collect::<Vec<_>>().join("|")
));
}
if rebalance && gross_exposure == 0.0 {
notes.push("risk throttle forced all-cash".to_string());
@@ -187,6 +230,7 @@ impl Strategy for CnSmallCapRotationStrategy {
target_weights,
exit_symbols,
notes,
diagnostics,
})
}
}

View File

@@ -14,6 +14,8 @@ pub struct UniverseCandidate {
pub symbol: String,
pub market_cap_bn: f64,
pub free_float_cap_bn: f64,
pub band_low: f64,
pub band_high: f64,
}
pub struct SelectionContext<'a> {
@@ -29,51 +31,54 @@ pub trait UniverseSelector {
#[derive(Debug, Clone)]
pub struct DynamicMarketCapBandSelector {
pub base_index_level: f64,
pub bullish_threshold: f64,
pub neutral_threshold: f64,
pub bullish_band: (f64, f64),
pub neutral_band: (f64, f64),
pub defensive_band: (f64, f64),
pub base_cap_floor: f64,
pub cap_span: f64,
pub xs: f64,
pub top_n: usize,
}
impl DynamicMarketCapBandSelector {
pub fn demo(top_n: usize) -> Self {
pub fn new(
base_index_level: f64,
base_cap_floor: f64,
cap_span: f64,
xs: f64,
top_n: usize,
) -> Self {
Self {
base_index_level: 3000.0,
bullish_threshold: 1.02,
neutral_threshold: 1.0,
bullish_band: (30.0, 60.0),
neutral_band: (40.0, 90.0),
defensive_band: (60.0, 120.0),
base_index_level,
base_cap_floor,
cap_span,
xs,
top_n,
}
}
pub fn demo(top_n: usize) -> Self {
Self::new(2000.0, 7.0, 10.0, 4.0 / 500.0, top_n)
}
pub fn regime(&self, benchmark_level: f64) -> BandRegime {
let ratio = benchmark_level / self.base_index_level;
if ratio >= self.bullish_threshold {
if benchmark_level >= self.base_index_level + 400.0 {
BandRegime::Bullish
} else if ratio >= self.neutral_threshold {
} else if benchmark_level >= self.base_index_level {
BandRegime::Neutral
} else {
BandRegime::Defensive
}
}
fn band(&self, regime: BandRegime) -> (f64, f64) {
match regime {
BandRegime::Bullish => self.bullish_band,
BandRegime::Neutral => self.neutral_band,
BandRegime::Defensive => self.defensive_band,
}
pub fn band_for_level(&self, benchmark_level: f64) -> (f64, f64) {
let start = ((benchmark_level - self.base_index_level) * self.xs) + self.base_cap_floor;
let low = start.round();
(low, low + self.cap_span)
}
}
impl UniverseSelector for DynamicMarketCapBandSelector {
fn select(&self, ctx: &SelectionContext<'_>) -> Vec<UniverseCandidate> {
let regime = self.regime(ctx.benchmark.close);
let (min_cap, max_cap) = self.band(regime);
let _regime = self.regime(ctx.benchmark.close);
let (min_cap, max_cap) = self.band_for_level(ctx.benchmark.close);
let mut selected = ctx
.data
@@ -94,6 +99,8 @@ impl UniverseSelector for DynamicMarketCapBandSelector {
symbol: factor.symbol.clone(),
market_cap_bn: factor.market_cap_bn,
free_float_cap_bn: factor.free_float_cap_bn,
band_low: min_cap,
band_high: max_cap,
})
})
.collect::<Vec<_>>();

View File

@@ -23,6 +23,8 @@ fn candidate() -> CandidateEligibility {
is_paused: false,
allow_buy: true,
allow_sell: true,
is_kcb: false,
is_one_yuan: false,
}
}

View File

@@ -0,0 +1,34 @@
use chrono::NaiveDate;
use fidc_core::{CnSmallCapRotationConfig, CnSmallCapRotationStrategy, DataSet, Strategy, StrategyContext, PortfolioState};
use std::path::PathBuf;
#[test]
fn strategy_emits_target_weights_and_diagnostics() {
let data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../data/demo");
let data = DataSet::from_csv_dir(&data_dir).expect("demo data");
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::demo();
cfg.base_index_level = 3000.0;
cfg.base_cap_floor = 38.0;
cfg.cap_span = 25.0;
let mut strategy = CnSmallCapRotationStrategy::new(cfg);
let decision = strategy
.on_day(&StrategyContext {
execution_date,
decision_date,
decision_index: 0,
data: &data,
portfolio: &portfolio,
})
.expect("decision");
assert!(decision.rebalance);
assert!(!decision.target_weights.is_empty());
assert!(decision
.diagnostics
.iter()
.any(|line| line.contains("selected=")));
}

View File

@@ -1,37 +1,37 @@
date,symbol,is_st,is_new_listing,is_paused,allow_buy,allow_sell
2024-01-02,000001.SZ,false,true,false,false,true
2024-01-02,000002.SZ,false,false,false,true,true
2024-01-02,000003.SZ,false,false,false,true,true
2024-01-02,600001.SH,false,false,false,true,true
2024-01-03,000001.SZ,false,true,false,false,true
2024-01-03,000002.SZ,false,false,false,true,true
2024-01-03,000003.SZ,false,false,false,true,true
2024-01-03,600001.SH,false,false,false,true,true
2024-01-04,000001.SZ,false,false,false,true,true
2024-01-04,000002.SZ,false,false,false,true,true
2024-01-04,000003.SZ,false,false,false,true,true
2024-01-04,600001.SH,false,false,false,true,true
2024-01-05,000001.SZ,false,false,false,true,true
2024-01-05,000002.SZ,false,false,false,true,true
2024-01-05,000003.SZ,false,false,false,true,true
2024-01-05,600001.SH,false,false,false,true,true
2024-01-08,000001.SZ,false,false,false,true,true
2024-01-08,000002.SZ,false,false,false,true,true
2024-01-08,000003.SZ,false,false,false,true,true
2024-01-08,600001.SH,false,false,false,true,true
2024-01-09,000001.SZ,false,false,false,true,true
2024-01-09,000002.SZ,false,false,false,true,true
2024-01-09,000003.SZ,false,false,false,true,true
2024-01-09,600001.SH,false,false,false,true,true
2024-01-10,000001.SZ,false,false,false,true,true
2024-01-10,000002.SZ,false,false,false,true,true
2024-01-10,000003.SZ,false,false,false,true,true
2024-01-10,600001.SH,false,false,false,true,true
2024-01-11,000001.SZ,false,false,false,true,true
2024-01-11,000002.SZ,false,false,false,true,true
2024-01-11,000003.SZ,false,false,false,true,true
2024-01-11,600001.SH,false,false,true,false,false
2024-01-12,000001.SZ,false,false,false,true,true
2024-01-12,000002.SZ,false,false,false,true,true
2024-01-12,000003.SZ,false,false,false,true,true
2024-01-12,600001.SH,false,false,false,true,true
date,symbol,is_st,is_new_listing,is_paused,allow_buy,allow_sell,is_kcb,is_one_yuan
2024-01-02,000001.SZ,false,true,false,false,true,false,false
2024-01-02,000002.SZ,false,false,false,true,true,false,false
2024-01-02,000003.SZ,false,false,false,true,true,false,false
2024-01-02,600001.SH,false,false,false,true,true,false,false
2024-01-03,000001.SZ,false,true,false,false,true,false,false
2024-01-03,000002.SZ,false,false,false,true,true,false,false
2024-01-03,000003.SZ,false,false,false,true,true,false,false
2024-01-03,600001.SH,false,false,false,true,true,false,false
2024-01-04,000001.SZ,false,false,false,true,true,false,false
2024-01-04,000002.SZ,false,false,false,true,true,false,false
2024-01-04,000003.SZ,false,false,false,true,true,false,false
2024-01-04,600001.SH,false,false,false,true,true,false,false
2024-01-05,000001.SZ,false,false,false,true,true,false,false
2024-01-05,000002.SZ,false,false,false,true,true,false,false
2024-01-05,000003.SZ,false,false,false,true,true,false,false
2024-01-05,600001.SH,false,false,false,true,true,false,false
2024-01-08,000001.SZ,false,false,false,true,true,false,false
2024-01-08,000002.SZ,false,false,false,true,true,false,false
2024-01-08,000003.SZ,false,false,false,true,true,false,false
2024-01-08,600001.SH,false,false,false,true,true,false,false
2024-01-09,000001.SZ,false,false,false,true,true,false,false
2024-01-09,000002.SZ,false,false,false,true,true,false,false
2024-01-09,000003.SZ,false,false,false,true,true,false,false
2024-01-09,600001.SH,false,false,false,true,true,false,false
2024-01-10,000001.SZ,false,false,false,true,true,false,false
2024-01-10,000002.SZ,false,false,false,true,true,false,false
2024-01-10,000003.SZ,false,false,false,true,true,false,false
2024-01-10,600001.SH,false,false,false,true,true,false,false
2024-01-11,000001.SZ,false,false,false,true,true,false,false
2024-01-11,000002.SZ,false,false,false,true,true,false,false
2024-01-11,000003.SZ,false,false,false,true,true,false,false
2024-01-11,600001.SH,false,false,true,false,false,false,false
2024-01-12,000001.SZ,false,false,false,true,true,false,false
2024-01-12,000002.SZ,false,false,false,true,true,false,false
2024-01-12,000003.SZ,false,false,false,true,true,false,false
2024-01-12,600001.SH,false,false,false,true,true,false,false
1 date symbol is_st is_new_listing is_paused allow_buy allow_sell is_kcb is_one_yuan
2 2024-01-02 000001.SZ false true false false true false false
3 2024-01-02 000002.SZ false false false true true false false
4 2024-01-02 000003.SZ false false false true true false false
5 2024-01-02 600001.SH false false false true true false false
6 2024-01-03 000001.SZ false true false false true false false
7 2024-01-03 000002.SZ false false false true true false false
8 2024-01-03 000003.SZ false false false true true false false
9 2024-01-03 600001.SH false false false true true false false
10 2024-01-04 000001.SZ false false false true true false false
11 2024-01-04 000002.SZ false false false true true false false
12 2024-01-04 000003.SZ false false false true true false false
13 2024-01-04 600001.SH false false false true true false false
14 2024-01-05 000001.SZ false false false true true false false
15 2024-01-05 000002.SZ false false false true true false false
16 2024-01-05 000003.SZ false false false true true false false
17 2024-01-05 600001.SH false false false true true false false
18 2024-01-08 000001.SZ false false false true true false false
19 2024-01-08 000002.SZ false false false true true false false
20 2024-01-08 000003.SZ false false false true true false false
21 2024-01-08 600001.SH false false false true true false false
22 2024-01-09 000001.SZ false false false true true false false
23 2024-01-09 000002.SZ false false false true true false false
24 2024-01-09 000003.SZ false false false true true false false
25 2024-01-09 600001.SH false false false true true false false
26 2024-01-10 000001.SZ false false false true true false false
27 2024-01-10 000002.SZ false false false true true false false
28 2024-01-10 000003.SZ false false false true true false false
29 2024-01-10 600001.SH false false false true true false false
30 2024-01-11 000001.SZ false false false true true false false
31 2024-01-11 000002.SZ false false false true true false false
32 2024-01-11 000003.SZ false false false true true false false
33 2024-01-11 600001.SH false false true false false false false
34 2024-01-12 000001.SZ false false false true true false false
35 2024-01-12 000002.SZ false false false true true false false
36 2024-01-12 000003.SZ false false false true true false false
37 2024-01-12 600001.SH false false false true true false false