Files
cargo-cxai-energy-0.1.0/src/market.rs
T
2026-04-26 16:48:11 +00:00

131 lines
3.6 KiB
Rust

use cxai_sdk::models::energy::*;
use serde::{Deserialize, Serialize};
/// Market analysis utilities for ERCOT data.
pub struct MarketAnalyzer;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpreadAnalysis {
pub avg_dam: f64,
pub avg_rt: f64,
pub spread: f64,
pub max_spread: f64,
pub arbitrage_opportunities: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RenewableMix {
pub wind_mw: f64,
pub solar_mw: f64,
pub total_renewable_mw: f64,
pub demand_mw: f64,
pub renewable_percentage: f64,
}
impl MarketAnalyzer {
/// Calculate DAM/RT price spread.
pub fn spread(dam_prices: &[DamPrice], rt_prices: &[RtPrice]) -> SpreadAnalysis {
let avg_dam = if dam_prices.is_empty() {
0.0
} else {
dam_prices.iter().map(|p| p.price).sum::<f64>() / dam_prices.len() as f64
};
let avg_rt = if rt_prices.is_empty() {
0.0
} else {
rt_prices.iter().map(|p| p.price).sum::<f64>() / rt_prices.len() as f64
};
let spreads: Vec<f64> = dam_prices
.iter()
.zip(rt_prices.iter())
.map(|(d, r)| (d.price - r.price).abs())
.collect();
let max_spread = spreads.iter().cloned().fold(0.0_f64, f64::max);
let arbitrage_opportunities = spreads.iter().filter(|&&s| s > 10.0).count();
SpreadAnalysis {
avg_dam,
avg_rt,
spread: avg_dam - avg_rt,
max_spread,
arbitrage_opportunities,
}
}
/// Calculate renewable energy mix.
pub fn renewable_mix(
wind: &WindGeneration,
solar: &SolarGeneration,
demand: &SystemDemand,
) -> RenewableMix {
let total = wind.generation_mw + solar.generation_mw;
let pct = if demand.demand_mw > 0.0 {
(total / demand.demand_mw) * 100.0
} else {
0.0
};
RenewableMix {
wind_mw: wind.generation_mw,
solar_mw: solar.generation_mw,
total_renewable_mw: total,
demand_mw: demand.demand_mw,
renewable_percentage: pct,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{NaiveDate, Utc};
#[test]
fn spread_calculation() {
let dam = vec![DamPrice {
settlement_point: "HB_HOUSTON".into(),
delivery_date: NaiveDate::from_ymd_opt(2026, 4, 20).unwrap(),
hour_ending: 14,
price: 45.0,
timestamp: None,
}];
let rt = vec![RtPrice {
settlement_point: "HB_HOUSTON".into(),
price: 52.0,
interval: "15min".into(),
timestamp: Utc::now(),
}];
let analysis = MarketAnalyzer::spread(&dam, &rt);
assert_eq!(analysis.avg_dam, 45.0);
assert_eq!(analysis.avg_rt, 52.0);
assert!((analysis.spread - (-7.0)).abs() < f64::EPSILON);
}
#[test]
fn renewable_mix_calculation() {
let wind = WindGeneration {
generation_mw: 15000.0,
capacity_mw: 40000.0,
timestamp: Utc::now(),
};
let solar = SolarGeneration {
generation_mw: 8000.0,
capacity_mw: 20000.0,
timestamp: Utc::now(),
};
let demand = SystemDemand {
demand_mw: 60000.0,
timestamp: Utc::now(),
forecast_mw: None,
};
let mix = MarketAnalyzer::renewable_mix(&wind, &solar, &demand);
assert_eq!(mix.total_renewable_mw, 23000.0);
assert!((mix.renewable_percentage - 38.333333333333336).abs() < 0.001);
}
}