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 <noreply@anthropic.com>
This commit is contained in:
209
ml/src/asset_selection/mod.rs
Normal file
209
ml/src/asset_selection/mod.rs
Normal file
@@ -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<String>,
|
||||
/// 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<UniverseAsset>,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
1
ml/src/asset_selection/scorer.rs
Normal file
1
ml/src/asset_selection/scorer.rs
Normal file
@@ -0,0 +1 @@
|
||||
//! Scorer module - TBD
|
||||
1
ml/src/asset_selection/selector.rs
Normal file
1
ml/src/asset_selection/selector.rs
Normal file
@@ -0,0 +1 @@
|
||||
//! Selector module - TBD
|
||||
@@ -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 ==========
|
||||
|
||||
|
||||
Reference in New Issue
Block a user