移除FIDC选股CSV覆盖入口

This commit is contained in:
boris
2026-07-03 09:51:39 +08:00
parent 3bb001c374
commit 5bbe8959f4
2 changed files with 20 additions and 297 deletions
+1 -297
View File
@@ -1,8 +1,4 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime}; use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime};
@@ -1694,12 +1690,6 @@ pub struct OmniMicroCapStrategy {
config: OmniMicroCapConfig, config: OmniMicroCapConfig,
} }
#[derive(Debug, Clone)]
struct OmniTruthStockLists {
source_path: String,
symbols_by_date: BTreeMap<NaiveDate, Vec<String>>,
}
#[derive(Default)] #[derive(Default)]
struct ProjectedExecutionState { struct ProjectedExecutionState {
execution_cursors: BTreeMap<String, NaiveDateTime>, execution_cursors: BTreeMap<String, NaiveDateTime>,
@@ -1719,23 +1709,6 @@ impl OmniMicroCapStrategy {
Self { config } Self { config }
} }
fn truth_stock_list_for_date(&self, date: NaiveDate) -> Option<&Vec<String>> {
omni_truth_stock_lists()
.as_ref()
.and_then(|lists| lists.symbols_by_date.get(&date))
}
fn truth_stock_list_source_path(&self) -> Option<&str> {
omni_truth_stock_lists()
.as_ref()
.map(|lists| lists.source_path.as_str())
}
fn truth_selection_contains(&self, date: NaiveDate, symbol: &str) -> bool {
self.truth_stock_list_for_date(date)
.is_some_and(|symbols| symbols.iter().any(|item| item == symbol))
}
fn stop_loss_tolerance(&self, market: &crate::data::DailyMarketSnapshot) -> f64 { fn stop_loss_tolerance(&self, market: &crate::data::DailyMarketSnapshot) -> f64 {
let _ = market; let _ = market;
0.0 0.0
@@ -2438,9 +2411,7 @@ impl OmniMicroCapStrategy {
) { ) {
return Ok(Some(reason.to_string())); return Ok(Some(reason.to_string()));
} }
if !self.truth_selection_contains(date, symbol) if !self.stock_passes_ma_filter(ctx, date, symbol) {
&& !self.stock_passes_ma_filter(ctx, date, symbol)
{
return Ok(Some("ma_filter".to_string())); return Ok(Some("ma_filter".to_string()));
} }
Ok(None) Ok(None)
@@ -2511,78 +2482,6 @@ impl OmniMicroCapStrategy {
band_low: f64, band_low: f64,
band_high: f64, band_high: f64,
) -> Result<(Vec<String>, Vec<String>), BacktestError> { ) -> Result<(Vec<String>, Vec<String>), BacktestError> {
if let Some(truth_symbols) = self.truth_stock_list_for_date(date) {
let mut diagnostics = vec![format!(
"selection_source=truth_csv path={} truth_candidates={}",
self.truth_stock_list_source_path().unwrap_or("<unknown>"),
truth_symbols.len()
)];
let mut selected = Vec::new();
let mut selected_set = BTreeSet::new();
let mut truth_selected = 0usize;
for symbol in truth_symbols {
if selected.len() >= self.config.stocknum {
break;
}
if !selected_set.insert(symbol.clone()) {
continue;
}
if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(symbol) {
selected_set.remove(symbol);
if diagnostics.len() < 14 {
diagnostics.push(format!("truth {} rejected by dynamic_universe", symbol));
}
continue;
}
if let Some(reason) = self.buy_rejection_reason(ctx, date, symbol)? {
selected_set.remove(symbol);
if diagnostics.len() < 14 {
diagnostics.push(format!("truth {} rejected by {}", symbol, reason));
}
continue;
}
selected.push(symbol.clone());
truth_selected += 1;
}
if selected.len() < self.config.stocknum {
let universe =
ctx.eligible_universe_on_with_risk_config(date, &self.config.risk_config);
let start = lower_bound_eligible(&universe, band_low);
for candidate in universe.iter().skip(start) {
if candidate.market_cap_bn > band_high {
break;
}
if selected.len() >= self.config.stocknum {
break;
}
if !selected_set.insert(candidate.symbol.clone()) {
continue;
}
if let Some(reason) = self.buy_rejection_reason(ctx, date, &candidate.symbol)? {
selected_set.remove(&candidate.symbol);
if diagnostics.len() < 18 {
diagnostics.push(format!(
"fallback {} rejected by {}",
candidate.symbol, reason
));
}
continue;
}
selected.push(candidate.symbol.clone());
}
}
diagnostics.push(format!(
"truth_selected={} fallback_selected={} requested={}",
truth_selected,
selected.len().saturating_sub(truth_selected),
self.config.stocknum
));
return Ok((selected, diagnostics));
}
let universe = ctx.eligible_universe_on_with_risk_config(date, &self.config.risk_config); let universe = ctx.eligible_universe_on_with_risk_config(date, &self.config.risk_config);
let mut diagnostics = Vec::new(); let mut diagnostics = Vec::new();
let mut selected = Vec::new(); let mut selected = Vec::new();
@@ -2609,156 +2508,6 @@ impl OmniMicroCapStrategy {
} }
} }
fn omni_truth_stock_lists() -> &'static Option<OmniTruthStockLists> {
static LISTS: OnceLock<Option<OmniTruthStockLists>> = OnceLock::new();
LISTS.get_or_init(load_omni_truth_stock_lists)
}
fn load_omni_truth_stock_lists() -> Option<OmniTruthStockLists> {
for path in omni_truth_stock_list_candidates() {
if !path.is_file() {
continue;
}
if let Ok(Some(lists)) = load_omni_truth_stock_lists_from_path(&path) {
return Some(lists);
}
}
None
}
fn omni_truth_stock_list_candidates() -> Vec<PathBuf> {
let mut candidates = Vec::new();
for key in [
"FIDC_BT_TRUTH_STOCK_LIST_CSV",
"OMNI_BT_TRUTH_STOCK_LIST_CSV",
"OMNI_BACKTEST_TRUTH_STOCK_LIST_CSV",
] {
if let Ok(value) = env::var(key) {
let trimmed = value.trim();
if !trimmed.is_empty() {
push_unique_truth_path(&mut candidates, PathBuf::from(trimmed));
}
}
}
candidates
}
fn push_unique_truth_path(paths: &mut Vec<PathBuf>, candidate: PathBuf) {
if !paths.iter().any(|existing| existing == &candidate) {
paths.push(candidate);
}
}
fn load_omni_truth_stock_lists_from_path(
path: &Path,
) -> Result<Option<OmniTruthStockLists>, String> {
let text = fs::read_to_string(path)
.map_err(|error| format!("read {} failed: {}", path.display(), error))?;
let mut lines = text.lines().filter(|line| !line.trim().is_empty());
let Some(header_line) = lines.next() else {
return Ok(None);
};
let headers = split_simple_csv_line(header_line.trim_start_matches('\u{feff}'));
let trade_date_idx = headers
.iter()
.position(|field| field == "trade_date")
.ok_or_else(|| format!("missing trade_date column in {}", path.display()))?;
let symbol_idx = headers
.iter()
.position(|field| field == "symbol")
.ok_or_else(|| format!("missing symbol column in {}", path.display()))?;
let rank_idx = headers
.iter()
.position(|field| field == "rank")
.or_else(|| headers.iter().position(|field| field == "index"));
let mut rows_by_date: BTreeMap<NaiveDate, Vec<(usize, String)>> = BTreeMap::new();
for (offset, line) in lines.enumerate() {
let cols = split_simple_csv_line(line);
let date_raw = cols
.get(trade_date_idx)
.ok_or_else(|| format!("missing trade_date at {}:{}", path.display(), offset + 2))?;
let symbol_raw = cols
.get(symbol_idx)
.ok_or_else(|| format!("missing symbol at {}:{}", path.display(), offset + 2))?;
let trade_date = NaiveDate::parse_from_str(date_raw, "%Y-%m-%d").map_err(|error| {
format!(
"invalid trade_date at {}:{}: {}",
path.display(),
offset + 2,
error
)
})?;
let Some(symbol) = normalize_truth_symbol(symbol_raw) else {
return Err(format!(
"invalid symbol at {}:{}",
path.display(),
offset + 2
));
};
let rank = rank_idx
.and_then(|idx| cols.get(idx))
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or_else(|| {
rows_by_date
.get(&trade_date)
.map(|items| items.len() + 1)
.unwrap_or(1)
});
rows_by_date
.entry(trade_date)
.or_default()
.push((rank.max(1), symbol));
}
if rows_by_date.is_empty() {
return Ok(None);
}
let symbols_by_date = rows_by_date
.into_iter()
.map(|(date, mut rows)| {
rows.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
let mut seen = BTreeSet::new();
let ordered = rows
.into_iter()
.filter_map(|(_, symbol)| {
if seen.insert(symbol.clone()) {
Some(symbol)
} else {
None
}
})
.collect::<Vec<_>>();
(date, ordered)
})
.collect::<BTreeMap<_, _>>();
Ok(Some(OmniTruthStockLists {
source_path: path.display().to_string(),
symbols_by_date,
}))
}
fn split_simple_csv_line(line: &str) -> Vec<String> {
line.split(',')
.map(|field| field.trim().trim_matches('"').to_string())
.collect()
}
fn normalize_truth_symbol(raw: &str) -> Option<String> {
let normalized = raw
.trim()
.to_ascii_uppercase()
.replace(".XSHG", ".SH")
.replace(".XSHE", ".SZ");
if normalized.is_empty() {
None
} else {
Some(normalized)
}
}
impl Strategy for OmniMicroCapStrategy { impl Strategy for OmniMicroCapStrategy {
fn name(&self) -> &str { fn name(&self) -> &str {
self.config.strategy_name.as_str() self.config.strategy_name.as_str()
@@ -3021,51 +2770,6 @@ fn lower_bound_eligible(rows: &[crate::data::EligibleUniverseSnapshot], target:
mod tests { mod tests {
use super::*; use super::*;
use crate::{BenchmarkSnapshot, CandidateEligibility, DailyFactorSnapshot}; use crate::{BenchmarkSnapshot, CandidateEligibility, DailyFactorSnapshot};
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_csv_path(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
env::temp_dir().join(format!("{}_{}_{}.csv", name, std::process::id(), nanos))
}
#[test]
fn load_truth_stock_lists_preserves_rank_order() {
let path = temp_csv_path("omni_truth_list");
fs::write(
&path,
"trade_date,index,symbol\n2025-01-02,2,300935.SZ\n2025-01-02,1,300321.XSHE\n2025-01-02,1,300321.SZ\n",
)
.unwrap();
let lists = load_omni_truth_stock_lists_from_path(&path)
.unwrap()
.unwrap();
fs::remove_file(&path).ok();
let symbols = lists
.symbols_by_date
.get(&NaiveDate::from_ymd_opt(2025, 1, 2).unwrap())
.unwrap();
assert_eq!(
symbols,
&vec!["300321.SZ".to_string(), "300935.SZ".to_string()]
);
}
#[test]
fn normalize_truth_symbol_maps_external_platform_suffixes() {
assert_eq!(
normalize_truth_symbol("300321.XSHE").as_deref(),
Some("300321.SZ")
);
assert_eq!(
normalize_truth_symbol("603657.XSHG").as_deref(),
Some("603657.SH")
);
}
#[test] #[test]
fn omni_microcap_projection_uses_configured_trading_cost() { fn omni_microcap_projection_uses_configured_trading_cost() {
+19
View File
@@ -79,4 +79,23 @@ if [[ -n "$fused_hits" ]]; then
fail "exported fused tables are not allowed in fidc-backtest-engine runtime; use Strategy Factory Source Lake source rows directly" "$fused_hits" fail "exported fused tables are not allowed in fidc-backtest-engine runtime; use Strategy Factory Source Lake source rows directly" "$fused_hits"
fi fi
truth_csv_hits="$(
rg -n \
--glob '!**/.git/**' \
--glob '!**/target/**' \
--glob '!**/docs/**' \
--glob '!**/*.md' \
--glob '!**/tests/**' \
--glob '!**/*test*.rs' \
--glob '!crates/fidc-core/src/strategy_ai.rs' \
--glob '!scripts/verify-no-legacy-data-source.sh' \
'FIDC_BT_TRUTH_STOCK_LIST_CSV|OMNI_BT_TRUTH_STOCK_LIST_CSV|OMNI_BACKTEST_TRUTH_STOCK_LIST_CSV|selection_source=truth_csv|truth_stock_list|truth_csv' \
crates Cargo.toml \
2>/dev/null || true
)"
if [[ -n "$truth_csv_hits" ]]; then
fail "CSV truth stock-list overrides are not allowed in fidc-backtest-engine runtime; use Source Lake runtime spec selection only" "$truth_csv_hits"
fi
printf '[OK] fidc-backtest-engine has no legacy runtime data-source references\n' printf '[OK] fidc-backtest-engine has no legacy runtime data-source references\n'