Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions crates/core/src/activities/activities_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -756,12 +756,20 @@ impl ActivityService {
}
}

// 3. If exchange MIC is provided, it's an equity
// 3. OCC option symbol heuristic (e.g. AAPL240119C00150000)
// Must be checked before exchange MIC — search providers may attach an
// exchange MIC (e.g. "OPRA") to option symbols, which would otherwise
// cause them to be misclassified as equities.
if crate::utils::occ_symbol::looks_like_occ_symbol(&upper_symbol) {
return (AssetKind::Investment, Some(InstrumentType::Option));
}

// 4. If exchange MIC is provided, it's an equity
if exchange_mic.is_some() {
return (AssetKind::Investment, Some(InstrumentType::Equity));
}

// 4. Common crypto symbols heuristic (no MIC, bare symbol like BTC, ETH)
// 5. Common crypto symbols heuristic (no MIC, bare symbol like BTC, ETH)
let common_crypto = [
"BTC", "ETH", "XRP", "LTC", "BCH", "ADA", "DOT", "LINK", "XLM", "DOGE", "UNI", "SOL",
"AVAX", "MATIC", "ATOM", "ALGO", "VET", "FIL", "TRX", "ETC", "XMR", "AAVE", "MKR",
Expand All @@ -771,11 +779,6 @@ impl ActivityService {
return (AssetKind::Investment, Some(InstrumentType::Crypto));
}

// 5. OCC option symbol heuristic (e.g. AAPL240119C00150000)
if crate::utils::occ_symbol::looks_like_occ_symbol(&upper_symbol) {
return (AssetKind::Investment, Some(InstrumentType::Option));
}

// 6. Default to equity (most common case)
(AssetKind::Investment, Some(InstrumentType::Equity))
}
Expand Down Expand Up @@ -817,12 +820,46 @@ impl ActivityService {
.or_else(|| Some(format!("{}:{}", itype.as_db_str(), upper_symbol))),
});

// Fallback key for OCC option symbols that were previously misclassified
// as EQUITY due to exchange MIC taking priority over OCC heuristic.
// Must mirror the key format the old code would have produced (with MIC when present).
let fallback_equity_key = if matches!(instrument_type, Some(InstrumentType::Option)) {
exchange_mic
.filter(|mic| !mic.trim().is_empty())
.map(|mic| {
format!(
"{}:{}@{}",
InstrumentType::Equity.as_db_str(),
upper_symbol,
mic.trim().to_uppercase()
)
})
.or_else(|| {
Some(format!(
"{}:{}",
InstrumentType::Equity.as_db_str(),
upper_symbol,
))
})
} else {
None
};

if let Some(ref key) = expected_key {
// Pass 1: exact instrument key match
for asset in &assets {
if asset.instrument_key.as_deref() == Some(key) {
return Some(asset.id.clone());
}
}
// Pass 2: fallback for legacy misclassified options
if let Some(ref fallback) = fallback_equity_key {
for asset in &assets {
if asset.instrument_key.as_deref() == Some(fallback.as_str()) {
return Some(asset.id.clone());
}
}
}
}

for asset in &assets {
Expand Down
Loading