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
40 changes: 20 additions & 20 deletions crates/core/src/portfolio/valuation/valuation_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,42 +291,42 @@ impl ValuationServiceTrait for ValuationService {
.cloned()
.unwrap_or_default();

// Check for assets missing quotes that SHOULD have quotes
// (i.e., they have quotes elsewhere, just not for this date - indicates a gap)
// Assets with NO quotes at all are skipped - they'll be valued at ZERO
// and the health check will detect them
let missing_quotes_with_gap: Vec<_> = holdings_snapshot
// Count quotable positions (those with quotes somewhere in the range)
// and how many are missing a quote on this specific date.
let quotable_positions: Vec<_> = holdings_snapshot
.positions
.iter()
.filter(|(_, position)| !position.quantity.is_zero())
.map(|(symbol, _)| symbol)
// Only flag as missing if the asset HAS quotes (somewhere) but not for this date
.filter(|symbol| assets_with_quotes.contains(*symbol))
.cloned()
.collect();

let missing_quotes: Vec<_> = quotable_positions
.iter()
.filter(|symbol| !quotes_for_current_date.contains_key(*symbol))
.cloned()
.collect();

if !missing_quotes_with_gap.is_empty() {
// Full gap: no quotes at all for any quotable position → skip day
// to avoid recording a fake zero-value valuation.
if !quotable_positions.is_empty() && missing_quotes.len() == quotable_positions.len()
{
debug!(
"Quote gap for {:?} on {} (account '{}'). Skipping day.",
missing_quotes_with_gap, current_date, account_id_clone
"No quotes for any quotable position on {} (account '{}'). Skipping day.",
current_date, account_id_clone
);
return None;
}

// Check if there are any positions that need quotes
// but only consider positions that HAVE quotes somewhere
let has_quotable_positions = holdings_snapshot
.positions
.keys()
.any(|symbol| assets_with_quotes.contains(symbol));

if quotes_for_current_date.is_empty() && has_quotable_positions {
// Partial gap: some quotes present, some missing → proceed.
// Missing positions valued at ZERO by the calculator, which is
// better than dropping the entire day (see #683).
if !missing_quotes.is_empty() {
debug!(
"No quotes for date {} (account '{}'). Skipping day.",
current_date, account_id_clone
"Partial quote gap for {:?} on {} (account '{}').",
missing_quotes, current_date, account_id_clone
);
Comment on lines +325 to 329
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip valuation when a day has no quotes for all holdings

This block now logs missing quotes but always continues, so a date with no quotes for any quotable position is persisted as a real valuation where investments are valued at zero (because calculate_investment_market_value_acct treats missing quotes as zero). That creates artificial drawdowns at the start of history (before first available quote) or after quote backfill gaps, which is materially different from “partial gap” handling. Please keep the partial-gap change, but still guard the full-gap case (e.g., empty quotes_for_current_date with quotable positions) and skip that day.

Useful? React with 👍 / 👎.

return None;
}
let account_curr = &holdings_snapshot.currency;
if account_curr != &base_curr_clone
Expand Down
Loading