From ea6ac524bb3f33623a1e7c79fe26173194bb26d6 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sun, 1 Mar 2026 13:09:06 -0800 Subject: [PATCH] ai: skip fetch only if price:: set but no ticker:: alias --- src/analytics/performance.zig | 1 - src/models/classification.zig | 1 - src/models/portfolio.zig | 5 ++++ src/providers/twelvedata.zig | 52 ++++++++++++++++++++++++++--------- src/service.zig | 10 ++----- 5 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/analytics/performance.zig b/src/analytics/performance.zig index 91a5d9c..e24f6d1 100644 --- a/src/analytics/performance.zig +++ b/src/analytics/performance.zig @@ -498,4 +498,3 @@ test "as-of-date vs month-end -- different results from same data" { try std.testing.expect(me.one_year != null); try std.testing.expectApproxEqAbs(@as(f64, 0.15), me.one_year.?.total_return, 0.001); } - diff --git a/src/models/classification.zig b/src/models/classification.zig index 4d61aa9..ff6aede 100644 --- a/src/models/classification.zig +++ b/src/models/classification.zig @@ -9,7 +9,6 @@ /// symbol::02315N600,asset_class::US Large Cap,pct:num:55 /// symbol::02315N600,asset_class::International Developed,pct:num:20 /// symbol::02315N600,asset_class::Bonds,pct:num:15 - const std = @import("std"); /// A single classification entry for a symbol. diff --git a/src/models/portfolio.zig b/src/models/portfolio.zig index af57c1e..b7fa155 100644 --- a/src/models/portfolio.zig +++ b/src/models/portfolio.zig @@ -153,12 +153,17 @@ pub const Portfolio = struct { /// Get unique symbols for stock/ETF lots only (skips options, CDs, cash). /// Returns the price symbol (ticker alias if set, otherwise raw symbol). + /// Excludes manual-price-only lots (price:: set, no ticker::) since those + /// have no API coverage and should never be fetched. pub fn stockSymbols(self: Portfolio, allocator: std.mem.Allocator) ![][]const u8 { var seen = std.StringHashMap(void).init(allocator); defer seen.deinit(); for (self.lots) |lot| { if (lot.lot_type == .stock) { + // Skip lots that have a manual price but no ticker alias — + // these are securities without API coverage (e.g. 401k CIT shares). + if (lot.price != null and lot.ticker == null) continue; try seen.put(lot.priceSymbol(), {}); } } diff --git a/src/providers/twelvedata.zig b/src/providers/twelvedata.zig index a3a34bb..95896ba 100644 --- a/src/providers/twelvedata.zig +++ b/src/providers/twelvedata.zig @@ -98,19 +98,45 @@ pub const TwelveData = struct { return self.parsed.value.object; } - pub fn symbol(self: ParsedQuote) []const u8 { return jsonStr(self.root().get("symbol")); } - pub fn name(self: ParsedQuote) []const u8 { return jsonStr(self.root().get("name")); } - pub fn exchange(self: ParsedQuote) []const u8 { return jsonStr(self.root().get("exchange")); } - pub fn datetime(self: ParsedQuote) []const u8 { return jsonStr(self.root().get("datetime")); } - pub fn close(self: ParsedQuote) f64 { return parseJsonFloat(self.root().get("close")); } - pub fn open(self: ParsedQuote) f64 { return parseJsonFloat(self.root().get("open")); } - pub fn high(self: ParsedQuote) f64 { return parseJsonFloat(self.root().get("high")); } - pub fn low(self: ParsedQuote) f64 { return parseJsonFloat(self.root().get("low")); } - pub fn volume(self: ParsedQuote) u64 { return @intFromFloat(parseJsonFloat(self.root().get("volume"))); } - pub fn previous_close(self: ParsedQuote) f64 { return parseJsonFloat(self.root().get("previous_close")); } - pub fn change(self: ParsedQuote) f64 { return parseJsonFloat(self.root().get("change")); } - pub fn percent_change(self: ParsedQuote) f64 { return parseJsonFloat(self.root().get("percent_change")); } - pub fn average_volume(self: ParsedQuote) u64 { return @intFromFloat(parseJsonFloat(self.root().get("average_volume"))); } + pub fn symbol(self: ParsedQuote) []const u8 { + return jsonStr(self.root().get("symbol")); + } + pub fn name(self: ParsedQuote) []const u8 { + return jsonStr(self.root().get("name")); + } + pub fn exchange(self: ParsedQuote) []const u8 { + return jsonStr(self.root().get("exchange")); + } + pub fn datetime(self: ParsedQuote) []const u8 { + return jsonStr(self.root().get("datetime")); + } + pub fn close(self: ParsedQuote) f64 { + return parseJsonFloat(self.root().get("close")); + } + pub fn open(self: ParsedQuote) f64 { + return parseJsonFloat(self.root().get("open")); + } + pub fn high(self: ParsedQuote) f64 { + return parseJsonFloat(self.root().get("high")); + } + pub fn low(self: ParsedQuote) f64 { + return parseJsonFloat(self.root().get("low")); + } + pub fn volume(self: ParsedQuote) u64 { + return @intFromFloat(parseJsonFloat(self.root().get("volume"))); + } + pub fn previous_close(self: ParsedQuote) f64 { + return parseJsonFloat(self.root().get("previous_close")); + } + pub fn change(self: ParsedQuote) f64 { + return parseJsonFloat(self.root().get("change")); + } + pub fn percent_change(self: ParsedQuote) f64 { + return parseJsonFloat(self.root().get("percent_change")); + } + pub fn average_volume(self: ParsedQuote) u64 { + return @intFromFloat(parseJsonFloat(self.root().get("average_volume"))); + } pub fn fifty_two_week_low(self: ParsedQuote) f64 { const ftw = self.root().get("fifty_two_week") orelse return 0; diff --git a/src/service.zig b/src/service.zig index 8b6aea4..244cd59 100644 --- a/src/service.zig +++ b/src/service.zig @@ -138,8 +138,6 @@ pub const DataService = struct { const from = today.addDays(-365 * 10 - 60); const fetched = td.fetchCandles(self.allocator, symbol, from, today) catch { - // Write negative cache entry so we don't retry on next run - s.writeNegative(symbol, .candles_daily); return DataError.FetchFailed; }; @@ -171,7 +169,6 @@ pub const DataService = struct { var pg = try self.getPolygon(); const fetched = pg.fetchDividends(self.allocator, symbol, null, null) catch { - s.writeNegative(symbol, .dividends); return DataError.FetchFailed; }; @@ -202,7 +199,6 @@ pub const DataService = struct { var pg = try self.getPolygon(); const fetched = pg.fetchSplits(self.allocator, symbol) catch { - s.writeNegative(symbol, .splits); return DataError.FetchFailed; }; @@ -231,7 +227,6 @@ pub const DataService = struct { var cboe = self.getCboe(); const fetched = cboe.fetchOptionsChain(self.allocator, symbol) catch { - s.writeNegative(symbol, .options); return DataError.FetchFailed; }; @@ -266,7 +261,6 @@ pub const DataService = struct { const to = today.addDays(365); const fetched = fh.fetchEarnings(self.allocator, symbol, from, to) catch { - s.writeNegative(symbol, .earnings); return DataError.FetchFailed; }; @@ -297,7 +291,6 @@ pub const DataService = struct { var av = try self.getAlphaVantage(); const fetched = av.fetchEtfProfile(self.allocator, symbol) catch { - s.writeNegative(symbol, .etf_profile); return DataError.FetchFailed; }; @@ -412,9 +405,10 @@ pub const DataService = struct { } /// Read candles from cache only (no network fetch). Used by TUI for display. - /// Returns null if no cached data exists. + /// Returns null if no cached data exists or if the entry is a negative cache (fetch_failed). pub fn getCachedCandles(self: *DataService, symbol: []const u8) ?[]Candle { var s = self.store(); + if (s.isNegative(symbol, .candles_daily)) return null; const data = s.readRaw(symbol, .candles_daily) catch return null; if (data) |d| { defer self.allocator.free(d);