use serde::Deserialize;
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
use surface_lib::{MarketDataRow, OptimizationConfig};
#[derive(Debug, Deserialize)]
#[allow(dead_code)] struct CsvRow {
#[serde(rename = "symbol")]
symbol: String,
#[serde(rename = "snapshot_ts")]
snapshot_ts: String,
#[serde(rename = "option_type")]
option_type: String,
#[serde(rename = "strike_price")]
strike_price: f64,
#[serde(rename = "underlying_price")]
underlying_price: f64,
#[serde(rename = "years_to_exp")]
years_to_exp: f64,
#[serde(rename = "mark_iv")]
mark_iv: f64,
#[serde(rename = "open_interest", default)]
open_interest: f64,
#[serde(rename = "vega", default)]
vega: f64,
#[serde(rename = "expiration_ts", default)]
expiration_ts: Option<i64>,
}
fn extract_expiration_from_symbol(symbol: &str) -> Option<String> {
let parts: Vec<&str> = symbol.split('-').collect();
if parts.len() >= 3 {
Some(parts[1].to_string())
} else {
None
}
}
static EXPIRATION_MAPPING: OnceLock<Mutex<HashMap<i64, String>>> = OnceLock::new();
pub fn load_test_data(file_path: &str) -> Result<Vec<MarketDataRow>, Box<dyn std::error::Error>> {
let mut reader = csv::Reader::from_path(file_path)?;
let mut data = Vec::new();
let mut timestamp_to_expiration: HashMap<i64, String> = HashMap::new();
for result in reader.deserialize() {
let row: CsvRow = result?;
if let Some(expiration_str) = extract_expiration_from_symbol(&row.symbol) {
if let Some(timestamp) = row.expiration_ts {
timestamp_to_expiration.insert(timestamp, expiration_str);
}
}
let market_data = MarketDataRow {
option_type: row.option_type,
strike_price: row.strike_price,
underlying_price: row.underlying_price,
years_to_exp: row.years_to_exp,
market_iv: row.mark_iv / 100.0, vega: if row.vega > 0.0 { row.vega } else { 1.0 }, expiration: row.expiration_ts.unwrap_or_else(|| {
let seconds_per_year = 365.25 * 24.0 * 3600.0;
let base_timestamp = 1735689600; base_timestamp + (row.years_to_exp * seconds_per_year) as i64
}),
};
data.push(market_data);
}
let mapping = EXPIRATION_MAPPING.get_or_init(|| Mutex::new(HashMap::new()));
{
let mut guard = mapping.lock().unwrap();
*guard = timestamp_to_expiration;
}
Ok(data)
}
pub fn filter_by_expiration(data: Vec<MarketDataRow>, expiration_str: &str) -> Vec<MarketDataRow> {
let target_timestamp = match expiration_str {
"10JAN25" => 1736496000, "17JAN25" => 1737100800,
"24JAN25" => 1737705600,
"31JAN25" => 1738310400,
_ => {
eprintln!("Unknown expiration: {}", expiration_str);
return Vec::new();
}
};
data.into_iter()
.filter(|row| {
(row.expiration - target_timestamp).abs() < 86400
})
.collect()
}
pub fn get_available_expirations(data: &[MarketDataRow]) -> Vec<(i64, String, usize)> {
let mut expiration_counts = HashMap::new();
for row in data {
*expiration_counts.entry(row.expiration).or_insert(0) += 1;
}
let mut expirations: Vec<_> = expiration_counts.into_iter().collect();
expirations.sort_by_key(|(timestamp, _)| *timestamp);
expirations
.into_iter()
.map(|(timestamp, count)| {
let expiration_str = timestamp_to_expiration_string(timestamp);
(timestamp, expiration_str, count)
})
.collect()
}
fn timestamp_to_expiration_string(timestamp: i64) -> String {
if let Some(mapping) = EXPIRATION_MAPPING.get() {
let guard = mapping.lock().unwrap();
if let Some(expiration_str) = guard.get(×tamp) {
return expiration_str.clone();
}
for (&mapped_timestamp, expiration_str) in guard.iter() {
if (timestamp - mapped_timestamp).abs() < 86400 {
return expiration_str.clone();
}
}
}
format!("UNKNOWN_{}", timestamp)
}
pub fn create_test_config() -> OptimizationConfig {
OptimizationConfig::fast()
}
pub fn create_verbose_test_config() -> OptimizationConfig {
let mut config = OptimizationConfig::fast();
config.cmaes.verbosity = 2; config
}