From d2fe4c4e984633fcea885d26e812c02ea1064705 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Mon, 23 Feb 2026 13:23:42 +0100 Subject: [PATCH] feat(ml): add asset_selection module with AssetUniverse config types Introduces the asset_selection module with AssetClass enum (Equity/ETF/Future), UniverseAsset struct, and AssetUniverse with filtering, lookup, and a us_starter() preset of 7 highly liquid US assets. Includes scorer/selector placeholder stubs. Co-Authored-By: Claude Opus 4.6 --- ml/src/asset_selection/mod.rs | 209 +++++++++++++++++++++++++++++ ml/src/asset_selection/scorer.rs | 1 + ml/src/asset_selection/selector.rs | 1 + ml/src/lib.rs | 1 + 4 files changed, 212 insertions(+) create mode 100644 ml/src/asset_selection/mod.rs create mode 100644 ml/src/asset_selection/scorer.rs create mode 100644 ml/src/asset_selection/selector.rs diff --git a/ml/src/asset_selection/mod.rs b/ml/src/asset_selection/mod.rs new file mode 100644 index 000000000..0ebb3c352 --- /dev/null +++ b/ml/src/asset_selection/mod.rs @@ -0,0 +1,209 @@ +//! Asset universe configuration and selection for the trading pipeline. +//! +//! This module defines which assets are eligible for trading, including +//! their classification, exchange, sector, and minimum volume requirements. + +use serde::{Deserialize, Serialize}; + +pub mod scorer; +pub mod selector; + +/// Classification of a tradable asset. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AssetClass { + /// Individual equity (stock). + Equity, + /// Exchange-traded fund. + ETF, + /// Futures contract. + Future, +} + +/// A single asset in the trading universe. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UniverseAsset { + /// Ticker symbol (e.g. "AAPL"). + pub symbol: String, + /// MIC exchange code (e.g. "XNAS"). + pub exchange: String, + /// Asset classification. + pub asset_class: AssetClass, + /// Optional GICS sector. + pub sector: Option, + /// Minimum average daily volume required for eligibility. + pub min_daily_volume: f64, + /// Whether this asset is enabled for trading. + pub enabled: bool, +} + +/// The full set of assets available for trading. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AssetUniverse { + /// All configured assets. + pub assets: Vec, +} + +impl AssetUniverse { + /// Create an empty universe. + #[must_use] + pub fn new() -> Self { + Self { assets: Vec::new() } + } + + /// Return only enabled assets. + #[must_use] + pub fn eligible(&self) -> Vec<&UniverseAsset> { + self.assets.iter().filter(|a| a.enabled).collect() + } + + /// Count of enabled assets. + #[must_use] + pub fn eligible_count(&self) -> usize { + self.assets.iter().filter(|a| a.enabled).count() + } + + /// Find an asset by symbol (case-sensitive). + #[must_use] + pub fn find(&self, symbol: &str) -> Option<&UniverseAsset> { + self.assets.iter().find(|a| a.symbol == symbol) + } + + /// List all asset symbols. + #[must_use] + pub fn symbols(&self) -> Vec<&str> { + self.assets.iter().map(|a| a.symbol.as_str()).collect() + } + + /// Preset: US large-cap starter universe (7 highly liquid assets). + #[must_use] + pub fn us_starter() -> Self { + Self { + assets: vec![ + UniverseAsset { + symbol: "SPY".to_string(), + exchange: "XNYS".to_string(), + asset_class: AssetClass::ETF, + sector: None, + min_daily_volume: 50_000_000.0, + enabled: true, + }, + UniverseAsset { + symbol: "QQQ".to_string(), + exchange: "XNAS".to_string(), + asset_class: AssetClass::ETF, + sector: None, + min_daily_volume: 30_000_000.0, + enabled: true, + }, + UniverseAsset { + symbol: "AAPL".to_string(), + exchange: "XNAS".to_string(), + asset_class: AssetClass::Equity, + sector: Some("Technology".to_string()), + min_daily_volume: 50_000_000.0, + enabled: true, + }, + UniverseAsset { + symbol: "MSFT".to_string(), + exchange: "XNAS".to_string(), + asset_class: AssetClass::Equity, + sector: Some("Technology".to_string()), + min_daily_volume: 20_000_000.0, + enabled: true, + }, + UniverseAsset { + symbol: "NVDA".to_string(), + exchange: "XNAS".to_string(), + asset_class: AssetClass::Equity, + sector: Some("Technology".to_string()), + min_daily_volume: 30_000_000.0, + enabled: true, + }, + UniverseAsset { + symbol: "AMZN".to_string(), + exchange: "XNAS".to_string(), + asset_class: AssetClass::Equity, + sector: Some("Consumer".to_string()), + min_daily_volume: 20_000_000.0, + enabled: true, + }, + UniverseAsset { + symbol: "IWM".to_string(), + exchange: "XNYS".to_string(), + asset_class: AssetClass::ETF, + sector: None, + min_daily_volume: 20_000_000.0, + enabled: true, + }, + ], + } + } +} + +impl Default for AssetUniverse { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_us_starter_universe() { + let universe = AssetUniverse::us_starter(); + assert_eq!(universe.assets.len(), 7); + assert_eq!(universe.eligible_count(), 7); + } + + #[test] + fn test_eligible_filters_disabled() { + let mut universe = AssetUniverse::us_starter(); + if let Some(first) = universe.assets.get_mut(0) { + first.enabled = false; + } + assert_eq!(universe.eligible_count(), 6); + } + + #[test] + fn test_find_symbol() { + let universe = AssetUniverse::us_starter(); + let aapl = universe.find("AAPL"); + assert!(aapl.is_some()); + assert_eq!( + aapl.map(|a| a.exchange.as_str()).unwrap_or_default(), + "XNAS" + ); + } + + #[test] + fn test_find_missing_symbol() { + let universe = AssetUniverse::us_starter(); + assert!(universe.find("DOESNOTEXIST").is_none()); + } + + #[test] + fn test_symbols_list() { + let universe = AssetUniverse::us_starter(); + let symbols = universe.symbols(); + assert_eq!(symbols.len(), 7); + assert!(symbols.contains(&"SPY")); + assert!(symbols.contains(&"AAPL")); + } + + #[test] + fn test_empty_universe() { + let universe = AssetUniverse::new(); + assert!(universe.eligible().is_empty()); + } + + #[test] + fn test_serde_roundtrip() { + let universe = AssetUniverse::us_starter(); + let json = serde_json::to_string(&universe).unwrap_or_default(); + let restored: AssetUniverse = + serde_json::from_str(&json).unwrap_or_else(|_| AssetUniverse::new()); + assert_eq!(restored.assets.len(), 7); + } +} diff --git a/ml/src/asset_selection/scorer.rs b/ml/src/asset_selection/scorer.rs new file mode 100644 index 000000000..efe2cd35b --- /dev/null +++ b/ml/src/asset_selection/scorer.rs @@ -0,0 +1 @@ +//! Scorer module - TBD diff --git a/ml/src/asset_selection/selector.rs b/ml/src/asset_selection/selector.rs new file mode 100644 index 000000000..1d939de6d --- /dev/null +++ b/ml/src/asset_selection/selector.rs @@ -0,0 +1 @@ +//! Selector module - TBD diff --git a/ml/src/lib.rs b/ml/src/lib.rs index 52ed0827d..b569b98c1 100644 --- a/ml/src/lib.rs +++ b/ml/src/lib.rs @@ -833,6 +833,7 @@ pub mod data_validation; pub mod random_model; pub mod model_registry; pub mod data_pipeline; +pub mod asset_selection; // ========== MISSING TYPES STUBS ==========