From 8ce2d53d213d0587b3f7a52141b3eeb02c070794 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Mon, 23 Feb 2026 14:41:22 +0100 Subject: [PATCH] feat(ml): add trading_symbol field to UniverseAsset for micro contract mapping Co-Authored-By: Claude Opus 4.6 --- ml/src/asset_selection/mod.rs | 51 +++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/ml/src/asset_selection/mod.rs b/ml/src/asset_selection/mod.rs index d3438bd4a..509e702bb 100644 --- a/ml/src/asset_selection/mod.rs +++ b/ml/src/asset_selection/mod.rs @@ -25,9 +25,12 @@ pub enum AssetClass { /// A single asset in the trading universe. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UniverseAsset { - /// Ticker symbol (e.g. "AAPL"). + /// Ticker symbol (e.g. "ES.FUT") -- used for data/training. pub symbol: String, - /// MIC exchange code (e.g. "XNAS"). + /// Execution symbol (e.g. "MES.FUT") -- used for order routing. + /// When `None`, orders are routed to `symbol` directly. + pub trading_symbol: Option, + /// MIC exchange code (e.g. "GLBX.MDP3"). pub exchange: String, /// Asset classification. pub asset_class: AssetClass, @@ -39,6 +42,15 @@ pub struct UniverseAsset { pub enabled: bool, } +impl UniverseAsset { + /// The symbol to use for order execution. + /// Returns `trading_symbol` if set, otherwise `symbol`. + #[must_use] + pub fn execution_symbol(&self) -> &str { + self.trading_symbol.as_deref().unwrap_or(&self.symbol) + } +} + /// The full set of assets available for trading. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AssetUniverse { @@ -84,6 +96,7 @@ impl AssetUniverse { assets: vec![ UniverseAsset { symbol: "SPY".to_string(), + trading_symbol: None, exchange: "XNYS".to_string(), asset_class: AssetClass::ETF, sector: None, @@ -92,6 +105,7 @@ impl AssetUniverse { }, UniverseAsset { symbol: "QQQ".to_string(), + trading_symbol: None, exchange: "XNAS".to_string(), asset_class: AssetClass::ETF, sector: None, @@ -100,6 +114,7 @@ impl AssetUniverse { }, UniverseAsset { symbol: "AAPL".to_string(), + trading_symbol: None, exchange: "XNAS".to_string(), asset_class: AssetClass::Equity, sector: Some("Technology".to_string()), @@ -108,6 +123,7 @@ impl AssetUniverse { }, UniverseAsset { symbol: "MSFT".to_string(), + trading_symbol: None, exchange: "XNAS".to_string(), asset_class: AssetClass::Equity, sector: Some("Technology".to_string()), @@ -116,6 +132,7 @@ impl AssetUniverse { }, UniverseAsset { symbol: "NVDA".to_string(), + trading_symbol: None, exchange: "XNAS".to_string(), asset_class: AssetClass::Equity, sector: Some("Technology".to_string()), @@ -124,6 +141,7 @@ impl AssetUniverse { }, UniverseAsset { symbol: "AMZN".to_string(), + trading_symbol: None, exchange: "XNAS".to_string(), asset_class: AssetClass::Equity, sector: Some("Consumer".to_string()), @@ -132,6 +150,7 @@ impl AssetUniverse { }, UniverseAsset { symbol: "IWM".to_string(), + trading_symbol: None, exchange: "XNYS".to_string(), asset_class: AssetClass::ETF, sector: None, @@ -209,4 +228,32 @@ mod tests { serde_json::from_str(&json).unwrap_or_else(|_| AssetUniverse::new()); assert_eq!(restored.assets.len(), 7); } + + #[test] + fn test_trading_symbol_mapping() { + let asset = UniverseAsset { + symbol: "ES.FUT".to_string(), + trading_symbol: Some("MES.FUT".to_string()), + exchange: "GLBX.MDP3".to_string(), + asset_class: AssetClass::Future, + sector: None, + min_daily_volume: 1_000_000.0, + enabled: true, + }; + assert_eq!(asset.execution_symbol(), "MES.FUT"); + } + + #[test] + fn test_trading_symbol_none_falls_back() { + let asset = UniverseAsset { + symbol: "ZN.FUT".to_string(), + trading_symbol: None, + exchange: "GLBX.MDP3".to_string(), + asset_class: AssetClass::Future, + sector: None, + min_daily_volume: 500_000.0, + enabled: true, + }; + assert_eq!(asset.execution_symbol(), "ZN.FUT"); + } }