diff --git a/src/cache/store.zig b/src/cache/store.zig index 22845ee..0668952 100644 --- a/src/cache/store.zig +++ b/src/cache/store.zig @@ -1408,13 +1408,28 @@ pub const Store = struct { /// (SRF returns FieldNotFoundOnFieldWithoutDefaultValue). /// `readCandleMeta` swallows the error and returns null, /// making the symbol look like a cache miss - `getCandles` - /// then triggers a fresh fetch via `populateAllFromTiingo`, + /// then triggers a fresh fetch via `refetchFullHistory`, /// which writes a new meta file with the provider explicit. /// The wipe happens naturally on first use post-upgrade. + /// + /// This is also why no *other* field on this struct may be + /// default-less: that cold-start path is the destructive one. + /// It writes a negative-cache marker over `candles_daily.srf` + /// when no provider carries the symbol, so routing a + /// populated cache through it risks discarding real history. + /// Fields added since (`tiingo_retry_after_s`, `adj_basis`) + /// are defaulted so legacy caches keep parsing. provider: CandleProvider, - /// Consecutive transient failure count for the primary provider (Tiingo). - /// Incremented on ServerError; reset to 0 on success. When >= 3, the - /// symbol is degraded to a fallback provider until Tiingo recovers. + /// Consecutive transient-failure count for a candle fetch + /// (ServerError / connection failure). Reset to 0 on any + /// successful fetch. At >= 3, `getCandles` serves the stale + /// cached series rather than returning an error, so a provider + /// outage degrades to "slightly old prices" instead of "no + /// prices". + /// + /// It does **not** switch providers. Provider selection is + /// `tiingo_retry_after_s`'s job, and only a genuine 404 moves + /// it - a transient outage says nothing about coverage. fail_count: u8 = 0, /// Unix-seconds instant before which Tiingo should not be /// consulted for this symbol. `0` (the default) means "no @@ -1492,17 +1507,22 @@ pub const Store = struct { /// writes produce this value (TwelveData was demoted in an /// earlier change because its `adj_close` was unreliable). /// Cache reads still recognize the value for backwards - /// compatibility. + /// compatibility, and `getCandles` treats such a cache as + /// unusable so the symbol gets refetched. twelvedata, - /// Legacy: candles were sourced from Yahoo Finance. No new - /// writes produce this value (Yahoo was removed from the - /// candle pipeline in the 2026-05 audit; Yahoo is still used - /// for `getQuote` real-time prices but not for historical - /// candles). Cache reads still recognize the value for - /// backwards compatibility. + /// Candles were sourced from Yahoo Finance. + /// + /// Actively written. `fetchCandlesFromProviders` falls back to + /// Yahoo whenever Tiingo cannot serve a symbol, and + /// `refetchFullHistory` does the same for a full restatement. + /// Yahoo's `adj_close` is split- and dividend-adjusted like + /// Tiingo's, but its parser yields 0 for a JSON `null` element, + /// which the analytics layer then has to discard - so Tiingo is + /// preferred where both will answer. yahoo, - /// Active: candles sourced from Tiingo. The only value - /// produced by current writes. + /// Candles were sourced from Tiingo. Preferred: it serves + /// dividends and splits from the same response, and its + /// `adj_close` is what the analytics layer is written against. tiingo, pub fn fromString(s: []const u8) CandleProvider { @@ -3684,7 +3704,7 @@ test "deserializeCandleMeta fails on old cache that elided provider field" { // (model has no default for provider). The graceful handling is // upstream: `readCandleMeta` swallows the deserialization error // and returns null, which makes `getCandles` treat it as a cache - // miss and trigger a fresh fetch via `populateAllFromTiingo`. + // miss and trigger a fresh fetch via `refetchFullHistory`. // The new fetch writes a meta file with the provider explicit. // // This test documents the failure mode and confirms it's not a diff --git a/src/commands/perf.zig b/src/commands/perf.zig index 14e61b2..d311eaa 100644 --- a/src/commands/perf.zig +++ b/src/commands/perf.zig @@ -83,10 +83,12 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void { try out.print(")\nLatest close: {f}\n", .{Money.from(c[c.len - 1].close)}); // `dividends != null` indicates we got explicit dividend records - // from the provider. When false we still display total return - // (synthesized from adj_close, which most providers bake dividends - // into), but we surface a hint that explicit dividend data is - // missing. + // from the provider. When false we still display total return - + // `performance.totalReturns` degrades to the provider's `adj_close`, + // which is dividend-adjusted - but we surface a hint, because that + // series is only as current as the last full candle fetch (see + // `CandleMeta.adj_basis`) and understates by the missed yield when a + // distribution has gone ex since. const has_explicit_divs = result.dividends != null; // -- As-of-date returns --