remove remaining AlphaVantage code

This commit is contained in:
Emil Lerch 2026-06-16 19:44:30 -07:00
parent 867f9afb8c
commit 78ffecba4f
Signed by: lobo
GPG key ID: A7B62D657EF764F8
3 changed files with 29 additions and 142 deletions

144
src/cache/store.zig vendored
View file

@ -9,9 +9,6 @@ const Dividend = @import("../models/dividend.zig").Dividend;
const DividendType = @import("../models/dividend.zig").DividendType;
const Split = @import("../models/split.zig").Split;
const EarningsEvent = @import("../models/earnings.zig").EarningsEvent;
const EtfProfile = @import("../models/etf_profile.zig").EtfProfile;
const Holding = @import("../models/etf_profile.zig").Holding;
const SectorWeight = @import("../models/etf_profile.zig").SectorWeight;
const Wikidata = @import("../providers/Wikidata.zig");
const Edgar = @import("../providers/Edgar.zig");
@ -45,8 +42,6 @@ pub const Ttl = struct {
pub const options: i64 = std.time.s_per_hour;
/// Earnings refresh monthly, with smart refresh after announcements
pub const earnings: i64 = 30 * s_per_day;
/// ETF profiles refresh monthly
pub const etf_profile: i64 = 30 * s_per_day;
/// Per-symbol classification record (sector / industry / country /
/// inception_date / CIK) sourced from Wikidata. The data changes
@ -171,7 +166,6 @@ pub const DataType = enum {
splits,
options,
earnings,
etf_profile,
meta,
/// Per-symbol classification record sourced from Wikidata.
/// Stored at `<cache_dir>/<symbol>/classification.srf`.
@ -204,7 +198,6 @@ pub const DataType = enum {
.splits => "splits.srf",
.options => "options.srf",
.earnings => "earnings.srf",
.etf_profile => "etf_profile.srf",
.meta => "meta.srf",
.classification => "classification.srf",
.etf_metrics => "etf_metrics.srf",
@ -231,7 +224,7 @@ pub const DataType = enum {
/// because the absolute spread on a 30d/90d base is
/// already large in days.
///
/// - 0% on the rest. options/earnings/etf_profile either
/// - 0% on the rest. options/earnings either
/// have natural cadence spread or are short-TTL enough
/// that jitter would exceed meaningful drift.
pub fn ttl(self: DataType) TtlSpec {
@ -240,7 +233,6 @@ pub const DataType = enum {
.splits => .{ .seconds = Ttl.splits, .jitter_pct = 11 },
.options => .{ .seconds = Ttl.options },
.earnings => .{ .seconds = Ttl.earnings },
.etf_profile => .{ .seconds = Ttl.etf_profile },
.classification => .{ .seconds = Ttl.classification, .jitter_pct = 8 },
.etf_metrics => .{ .seconds = Ttl.etf_metrics, .jitter_pct = 8 },
.entity_facts => .{ .seconds = Ttl.entity_facts, .jitter_pct = 8 },
@ -292,7 +284,6 @@ pub const Store = struct {
Split => .splits,
EarningsEvent => .earnings,
OptionsChain => .options,
EtfProfile => .etf_profile,
Wikidata.ClassificationRecord => .classification,
Edgar.EtfMetricRecord => .etf_metrics,
Edgar.EntityFactRecord => .entity_facts,
@ -302,9 +293,10 @@ pub const Store = struct {
};
}
/// The data payload for a given type: single struct for EtfProfile, slice for everything else.
/// The data payload for a given type. Every supported type is
/// cached as a slice of records.
pub fn DataFor(comptime T: type) type {
return if (T == EtfProfile) EtfProfile else []T;
return []T;
}
pub fn CacheResult(comptime T: type) type {
@ -345,15 +337,12 @@ pub const Store = struct {
return null;
}
if (T == EtfProfile or T == OptionsChain) {
if (T == OptionsChain) {
const is_negative = std.mem.eql(u8, data, negative_cache_content);
if (is_negative) {
if (freshness == .fresh_only) {
// Negative entries are always fresh return empty data
if (T == EtfProfile)
return .{ .data = EtfProfile{ .symbol = "" }, .timestamp = std.Io.Timestamp.now(self.io, .real).toSeconds() };
if (T == OptionsChain)
return .{ .data = &.{}, .timestamp = std.Io.Timestamp.now(self.io, .real).toSeconds() };
return .{ .data = &.{}, .timestamp = std.Io.Timestamp.now(self.io, .real).toSeconds() };
}
return null;
}
@ -368,21 +357,14 @@ pub const Store = struct {
}
const timestamp = it.created orelse std.Io.Timestamp.now(self.io, .real).toSeconds();
if (T == EtfProfile) {
const profile = deserializeEtfProfile(allocator, &it) catch return null;
return .{ .data = profile, .timestamp = timestamp };
}
if (T == OptionsChain) {
const items = deserializeOptions(allocator, &it) catch return null;
return .{ .data = items, .timestamp = timestamp };
}
const items = deserializeOptions(allocator, &it) catch return null;
return .{ .data = items, .timestamp = timestamp };
}
return readSlice(T, self.io, allocator, data, postProcess, freshness);
}
/// Serialize data and write to cache with the given TTL.
/// Accepts a slice for most types, or a single struct for EtfProfile.
///
/// For `Dividend` and `Split`, this dispatches to `writeMerged`,
/// which performs sorted-union-with-existing semantics rather than
@ -419,17 +401,6 @@ pub const Store = struct {
}
const expires = computeExpires(std.Io.Timestamp.now(self.io, .real).toSeconds(), ttl, symbol);
const data_type = dataTypeFor(T);
if (T == EtfProfile) {
const srf_data = serializeEtfProfile(self.io, self.allocator, items, .{ .expires = expires }) catch |err| {
log.warn("{s}: failed to serialize ETF profile: {s}", .{ symbol, @errorName(err) });
return;
};
defer self.allocator.free(srf_data);
self.writeRaw(symbol, data_type, srf_data) catch |err| {
log.warn("{s}: failed to write ETF profile to cache: {s}", .{ symbol, @errorName(err) });
};
return;
}
if (T == OptionsChain) {
const srf_data = serializeOptions(self.io, self.allocator, items, .{ .expires = expires }) catch |err| {
log.warn("{s}: failed to serialize options: {s}", .{ symbol, @errorName(err) });
@ -1370,10 +1341,6 @@ pub const Store = struct {
try std.testing.expect(!hasNoStringFields(EarningsEvent));
}
test "hasNoStringFields: EtfProfile has string fields -> false" {
try std.testing.expect(!hasNoStringFields(EtfProfile));
}
test "hasNoStringFields: synthetic shapes" {
// Pure ints/floats/bools/enums + Date should pass.
const Pure = struct {
@ -1759,88 +1726,6 @@ pub const Store = struct {
return chains.toOwnedSlice(allocator);
}
// Private serialization: ETF profile (bespoke)
const EtfRecord = union(enum) {
pub const srf_tag_field = "type";
meta: EtfProfile,
sector: SectorWeight,
holding: Holding,
};
fn serializeEtfProfile(io: std.Io, allocator: std.mem.Allocator, profile: EtfProfile, options: srf.FormatOptions) ![]const u8 {
var records: std.ArrayList(EtfRecord) = .empty;
defer records.deinit(allocator);
try records.append(allocator, .{ .meta = profile });
if (profile.sectors) |sectors| {
for (sectors) |s| try records.append(allocator, .{ .sector = s });
}
if (profile.holdings) |holdings| {
for (holdings) |h| try records.append(allocator, .{ .holding = h });
}
var aw: std.Io.Writer.Allocating = .init(allocator);
errdefer aw.deinit();
var opts = options;
opts.created = std.Io.Timestamp.now(io, .real).toSeconds();
try aw.writer.print("{f}", .{srf.fmt(EtfRecord, records.items, opts)});
return aw.toOwnedSlice();
}
fn deserializeEtfProfile(allocator: std.mem.Allocator, it: *srf.RecordIterator) !EtfProfile {
var profile = EtfProfile{ .symbol = "" };
var sectors: std.ArrayList(SectorWeight) = .empty;
errdefer {
for (sectors.items) |s| allocator.free(s.name);
sectors.deinit(allocator);
}
var holdings: std.ArrayList(Holding) = .empty;
errdefer {
for (holdings.items) |h| {
if (h.symbol) |s| allocator.free(s);
if (h.cusip) |c| allocator.free(c);
allocator.free(h.name);
}
holdings.deinit(allocator);
}
while (try it.next()) |fields| {
const etf_rec = fields.to(EtfRecord, .{}) catch continue;
switch (etf_rec) {
.meta => |m| {
profile = m;
},
.sector => |s| {
const duped = try allocator.dupe(u8, s.name);
try sectors.append(allocator, .{ .name = duped, .weight = s.weight });
},
.holding => |h| {
const duped_sym = if (h.symbol) |s| try allocator.dupe(u8, s) else null;
errdefer if (duped_sym) |s| allocator.free(s);
const duped_cusip = if (h.cusip) |c| try allocator.dupe(u8, c) else null;
errdefer if (duped_cusip) |c| allocator.free(c);
const duped_name = try allocator.dupe(u8, h.name);
errdefer allocator.free(duped_name);
try holdings.append(allocator, .{ .symbol = duped_sym, .name = duped_name, .weight = h.weight, .cusip = duped_cusip });
},
}
}
if (sectors.items.len > 0) {
profile.sectors = try sectors.toOwnedSlice(allocator);
} else {
sectors.deinit(allocator);
}
if (holdings.items.len > 0) {
profile.holdings = try holdings.toOwnedSlice(allocator);
} else {
holdings.deinit(allocator);
}
return profile;
}
};
/// Serialize a portfolio (list of lots) to SRF format.
@ -2741,9 +2626,8 @@ test "TTL constants are reasonable" {
// Options refresh hourly
try std.testing.expectEqual(@as(i64, std.time.s_per_hour), Ttl.options);
// Earnings and ETF profiles refresh monthly
// Earnings refresh monthly
try std.testing.expectEqual(@as(i64, 30 * std.time.s_per_day), Ttl.earnings);
try std.testing.expectEqual(@as(i64, 30 * std.time.s_per_day), Ttl.etf_profile);
// New types: classification (90d) and etf_metrics (90d) refresh
// quarterly; entity_facts (30d) refreshes monthly.
@ -2786,13 +2670,11 @@ test "DataType.ttl returns correct seconds and jitter policy" {
try std.testing.expectEqual(Ttl.tickers_companies, tc.seconds);
try std.testing.expectEqual(@as(u8, 8), tc.jitter_pct);
// No jitter: short-TTL types and etf_profile.
// No jitter: short-TTL types.
try std.testing.expectEqual(Ttl.options, DataType.options.ttl().seconds);
try std.testing.expectEqual(@as(u8, 0), DataType.options.ttl().jitter_pct);
try std.testing.expectEqual(Ttl.earnings, DataType.earnings.ttl().seconds);
try std.testing.expectEqual(@as(u8, 0), DataType.earnings.ttl().jitter_pct);
try std.testing.expectEqual(Ttl.etf_profile, DataType.etf_profile.ttl().seconds);
try std.testing.expectEqual(@as(u8, 0), DataType.etf_profile.ttl().jitter_pct);
// candles_daily, candles_meta, and meta have their own writers
// (`cacheCandles`, `writeNegative`); calling .ttl() on them is
@ -2806,7 +2688,6 @@ test "DataType.fileName returns correct file names" {
try std.testing.expectEqualStrings("splits.srf", DataType.splits.fileName());
try std.testing.expectEqualStrings("options.srf", DataType.options.fileName());
try std.testing.expectEqualStrings("earnings.srf", DataType.earnings.fileName());
try std.testing.expectEqualStrings("etf_profile.srf", DataType.etf_profile.fileName());
try std.testing.expectEqualStrings("meta.srf", DataType.meta.fileName());
try std.testing.expectEqualStrings("classification.srf", DataType.classification.fileName());
try std.testing.expectEqualStrings("etf_metrics.srf", DataType.etf_metrics.fileName());
@ -3100,13 +2981,10 @@ test "Store.dataTypeFor maps model types correctly" {
try std.testing.expectEqual(DataType.splits, Store.dataTypeFor(Split));
try std.testing.expectEqual(DataType.earnings, Store.dataTypeFor(EarningsEvent));
try std.testing.expectEqual(DataType.options, Store.dataTypeFor(OptionsChain));
try std.testing.expectEqual(DataType.etf_profile, Store.dataTypeFor(EtfProfile));
}
test "Store.DataFor returns correct types" {
// EtfProfile returns single struct, others return slices
try std.testing.expect(@TypeOf(Store.DataFor(EtfProfile)) == type);
try std.testing.expect(Store.DataFor(EtfProfile) == EtfProfile);
// Every supported type is cached as a slice of records.
try std.testing.expect(Store.DataFor(Candle) == []Candle);
try std.testing.expect(Store.DataFor(Dividend) == []Dividend);
try std.testing.expect(Store.DataFor(Split) == []Split);

View file

@ -43,7 +43,6 @@ const display_types = [_]DataType{
.splits,
.options,
.earnings,
.etf_profile,
};
const display_labels = [_][]const u8{
@ -52,7 +51,6 @@ const display_labels = [_][]const u8{
"splits",
"options",
"earnings",
"etf_profile",
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -208,6 +208,19 @@ pub const Source = enum {
fetched,
};
/// In-memory payload shape for a fetched type `T`.
///
/// Almost everything is a slice of records (`[]Candle`, `[]Dividend`,
/// ) the same shape the cache stores. `EtfProfile` is the lone
/// exception: `getEtfProfile` assembles a single struct from the
/// `etf_metrics` cache rather than returning a slice, so its payload
/// is the struct itself. The cache layer never stores `EtfProfile`
/// directly, which is why this single-struct knowledge lives here in
/// the fetch layer rather than in `Store.DataFor`.
fn PayloadFor(comptime T: type) type {
return if (T == EtfProfile) EtfProfile else []T;
}
/// Generic result type for all fetch operations: data payload + provenance metadata.
///
/// `data` is owned by `allocator` call `result.deinit()` to release
@ -217,7 +230,7 @@ pub const Source = enum {
/// allocator (e.g. an arena) differed from the service's allocator.
pub fn FetchResult(comptime T: type) type {
return struct {
data: cache.Store.DataFor(T),
data: PayloadFor(T),
source: Source,
timestamp: i64,
/// Allocator that owns `data`. Populated by the service on
@ -1874,10 +1887,10 @@ pub const DataService = struct {
// No proactive token-bucket limiter for these. Tiingo
// (candles) has a 1000/day quota enforced reactively
// via 429-then-backoff in `getCandles`; Wikidata
// (classification) has no published quota; the legacy
// `etf_profile` and `meta` types aren't fetched. Nothing
// useful to wait for at the call site, so report 0.
.candles_daily, .candles_meta, .classification, .etf_profile, .meta => 0,
// (classification) has no published quota; the `meta`
// type isn't fetched. Nothing useful to wait for at the
// call site, so report 0.
.candles_daily, .candles_meta, .classification, .meta => 0,
};
return if (ns == 0) 0 else @max(1, ns / std.time.ns_per_s);
}
@ -2759,7 +2772,6 @@ pub const DataService = struct {
.earnings => "/earnings",
.options => "/options",
.splits => "/splits",
.etf_profile => return false, // not served (replaced by etf_metrics)
.meta => return false,
.classification => "/classification",
.etf_metrics => "/etf_metrics",
@ -3876,7 +3888,6 @@ test "estimateWaitSeconds returns 0 for types without rate limiters" {
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.candles_daily));
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.candles_meta));
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.classification));
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.etf_profile));
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.meta));
}