use std::collections::{BTreeSet, HashMap};
use std::path::Path;
use pomelo_data::{
is_fundamental_series, list_symbols, load_combined_panel, load_fundamental_panel, load_panel,
Field, LocalSource, FUNDAMENTALS_DIR, PANELS_DIR, PRICES_DIR,
};
use yuzu_core::backtest::BacktestConfig;
use yuzu_core::EvalContext;
pub(crate) fn field_for_price_key(key: &str) -> Result<Field, String> {
match key {
"close" => Ok(Field::AdjClose),
"open" => Ok(Field::AdjOpen),
"high" => Ok(Field::AdjHigh),
"low" => Ok(Field::AdjLow),
other => Err(format!(
"price-key must be one of open/high/low/close (got '{other}')"
)),
}
}
pub(crate) fn load_ctx(
root: &Path,
from: i32,
to: i32,
cfg: &BacktestConfig,
price_key: &str,
symbols: Option<&[String]>,
referenced: &BTreeSet<String>,
) -> Result<EvalContext, String> {
let syms = scoped_symbols(root, symbols)?;
let src = LocalSource::new(root);
let mut panels = HashMap::new();
let close = load_panel(&src, &syms, Field::AdjClose, from, to, PRICES_DIR)
.map_err(|e| e.to_string())?;
panels.insert("close".to_string(), close);
let price_field = field_for_price_key(price_key)?;
if price_key != "close" {
let px = load_panel(&src, &syms, price_field, from, to, PRICES_DIR)
.map_err(|e| e.to_string())?;
panels.insert(price_key.to_string(), px);
}
if cfg.stops.is_active() {
for (name, field) in [
("open", Field::AdjOpen),
("high", Field::AdjHigh),
("low", Field::AdjLow),
] {
if !panels.contains_key(name) {
let p = load_panel(&src, &syms, field, from, to, PRICES_DIR)
.map_err(|e| e.to_string())?;
panels.insert(name.to_string(), p);
}
}
}
if (cfg.max_participation > 0.0 || cfg.impact_coef > 0.0) && cfg.initial_capital > 0.0 {
let volume = load_panel(&src, &syms, Field::Volume, from, to, PRICES_DIR)
.map_err(|e| e.to_string())?;
panels.insert("volume".to_string(), volume);
}
for name in pomelo_fmp::MEMBERSHIP_SERIES {
if panels.contains_key(*name) {
continue;
}
if let Some(p) = load_combined_panel(&src, name, &syms, from, to, PANELS_DIR)
.map_err(|e| e.to_string())?
{
panels.insert((*name).to_string(), p);
}
}
for name in pomelo_data::fundamentals::FACTOR_PANEL_FIELDS {
if panels.contains_key(*name) {
continue;
}
if let Some(p) = load_combined_panel(&src, name, &syms, from, to, PANELS_DIR)
.map_err(|e| e.to_string())?
{
panels.insert((*name).to_string(), p);
}
}
for name in referenced {
if panels.contains_key(name.as_str()) {
continue;
}
if let Some(p) = load_combined_panel(&src, name, &syms, from, to, PANELS_DIR)
.map_err(|e| e.to_string())?
{
panels.insert(name.clone(), p);
} else if let Some(field) = ohlcv_field(name) {
let p =
load_panel(&src, &syms, field, from, to, PRICES_DIR).map_err(|e| e.to_string())?;
panels.insert(name.clone(), p);
} else if is_fundamental_series(name) {
let p = load_fundamental_panel(&src, &syms, name, from, to, FUNDAMENTALS_DIR)
.map_err(|e| e.to_string())?;
panels.insert(name.clone(), p);
}
}
if let Some(sym) = &cfg.benchmark_key {
if !panels.contains_key(sym) {
let bench = load_panel(
&src,
std::slice::from_ref(sym),
Field::AdjClose,
from,
to,
PRICES_DIR,
)
.map_err(|e| e.to_string())?;
panels.insert(sym.clone(), bench);
}
}
let industry = pomelo_data::industry::load_industry_map(&src)
.map_err(|e| e.to_string())?
.unwrap_or_default();
Ok(EvalContext { panels, industry })
}
fn ohlcv_field(name: &str) -> Option<Field> {
match name {
"close" => Some(Field::AdjClose),
"open" => Some(Field::AdjOpen),
"high" => Some(Field::AdjHigh),
"low" => Some(Field::AdjLow),
"volume" => Some(Field::Volume),
_ => None,
}
}
pub(crate) fn referenced_series<S: AsRef<str>>(spec_jsons: &[S]) -> BTreeSet<String> {
fn walk(node: &serde_json::Value, out: &mut BTreeSet<String>) {
match node {
serde_json::Value::Object(map) => {
if map.get("op").and_then(serde_json::Value::as_str) == Some("Data") {
if let Some(name) = map.get("name").and_then(serde_json::Value::as_str) {
out.insert(name.to_string());
}
}
map.values().for_each(|v| walk(v, out));
}
serde_json::Value::Array(arr) => arr.iter().for_each(|v| walk(v, out)),
_ => {}
}
}
let mut out = BTreeSet::new();
for s in spec_jsons {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(s.as_ref()) {
walk(&v, &mut out);
}
}
out
}
fn scoped_symbols(root: &Path, symbols: Option<&[String]>) -> Result<Vec<String>, String> {
let available = list_symbols(root).map_err(|e| e.to_string())?;
let Some(requested) = symbols else {
return Ok(available);
};
if requested.is_empty() {
return Err("the symbols list is empty — omit it to run the full universe".into());
}
let have: std::collections::HashSet<&str> = available.iter().map(String::as_str).collect();
let missing: Vec<&str> = requested
.iter()
.map(String::as_str)
.filter(|s| !have.contains(s))
.collect();
if !missing.is_empty() {
return Err(format!(
"symbols not in the data tree (no prices/<sym>.csv.gz): {}",
missing.join(", ")
));
}
let mut syms: Vec<String> = requested.to_vec();
syms.sort();
syms.dedup();
Ok(syms)
}