ai: skip fetch only if price:: set but no ticker:: alias

This commit is contained in:
Emil Lerch 2026-03-01 13:09:06 -08:00
parent 6d9374ab77
commit ea6ac524bb
Signed by: lobo
GPG key ID: A7B62D657EF764F8
5 changed files with 46 additions and 23 deletions

View file

@ -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);
}

View file

@ -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.

View file

@ -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(), {});
}
}

View file

@ -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;

View file

@ -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);