remove remaining AlphaVantage code
This commit is contained in:
parent
867f9afb8c
commit
78ffecba4f
3 changed files with 29 additions and 142 deletions
144
src/cache/store.zig
vendored
144
src/cache/store.zig
vendored
|
|
@ -9,9 +9,6 @@ const Dividend = @import("../models/dividend.zig").Dividend;
|
||||||
const DividendType = @import("../models/dividend.zig").DividendType;
|
const DividendType = @import("../models/dividend.zig").DividendType;
|
||||||
const Split = @import("../models/split.zig").Split;
|
const Split = @import("../models/split.zig").Split;
|
||||||
const EarningsEvent = @import("../models/earnings.zig").EarningsEvent;
|
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 Wikidata = @import("../providers/Wikidata.zig");
|
||||||
const Edgar = @import("../providers/Edgar.zig");
|
const Edgar = @import("../providers/Edgar.zig");
|
||||||
|
|
||||||
|
|
@ -45,8 +42,6 @@ pub const Ttl = struct {
|
||||||
pub const options: i64 = std.time.s_per_hour;
|
pub const options: i64 = std.time.s_per_hour;
|
||||||
/// Earnings refresh monthly, with smart refresh after announcements
|
/// Earnings refresh monthly, with smart refresh after announcements
|
||||||
pub const earnings: i64 = 30 * s_per_day;
|
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 /
|
/// Per-symbol classification record (sector / industry / country /
|
||||||
/// inception_date / CIK) sourced from Wikidata. The data changes
|
/// inception_date / CIK) sourced from Wikidata. The data changes
|
||||||
|
|
@ -171,7 +166,6 @@ pub const DataType = enum {
|
||||||
splits,
|
splits,
|
||||||
options,
|
options,
|
||||||
earnings,
|
earnings,
|
||||||
etf_profile,
|
|
||||||
meta,
|
meta,
|
||||||
/// Per-symbol classification record sourced from Wikidata.
|
/// Per-symbol classification record sourced from Wikidata.
|
||||||
/// Stored at `<cache_dir>/<symbol>/classification.srf`.
|
/// Stored at `<cache_dir>/<symbol>/classification.srf`.
|
||||||
|
|
@ -204,7 +198,6 @@ pub const DataType = enum {
|
||||||
.splits => "splits.srf",
|
.splits => "splits.srf",
|
||||||
.options => "options.srf",
|
.options => "options.srf",
|
||||||
.earnings => "earnings.srf",
|
.earnings => "earnings.srf",
|
||||||
.etf_profile => "etf_profile.srf",
|
|
||||||
.meta => "meta.srf",
|
.meta => "meta.srf",
|
||||||
.classification => "classification.srf",
|
.classification => "classification.srf",
|
||||||
.etf_metrics => "etf_metrics.srf",
|
.etf_metrics => "etf_metrics.srf",
|
||||||
|
|
@ -231,7 +224,7 @@ pub const DataType = enum {
|
||||||
/// because the absolute spread on a 30d/90d base is
|
/// because the absolute spread on a 30d/90d base is
|
||||||
/// already large in days.
|
/// 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
|
/// have natural cadence spread or are short-TTL enough
|
||||||
/// that jitter would exceed meaningful drift.
|
/// that jitter would exceed meaningful drift.
|
||||||
pub fn ttl(self: DataType) TtlSpec {
|
pub fn ttl(self: DataType) TtlSpec {
|
||||||
|
|
@ -240,7 +233,6 @@ pub const DataType = enum {
|
||||||
.splits => .{ .seconds = Ttl.splits, .jitter_pct = 11 },
|
.splits => .{ .seconds = Ttl.splits, .jitter_pct = 11 },
|
||||||
.options => .{ .seconds = Ttl.options },
|
.options => .{ .seconds = Ttl.options },
|
||||||
.earnings => .{ .seconds = Ttl.earnings },
|
.earnings => .{ .seconds = Ttl.earnings },
|
||||||
.etf_profile => .{ .seconds = Ttl.etf_profile },
|
|
||||||
.classification => .{ .seconds = Ttl.classification, .jitter_pct = 8 },
|
.classification => .{ .seconds = Ttl.classification, .jitter_pct = 8 },
|
||||||
.etf_metrics => .{ .seconds = Ttl.etf_metrics, .jitter_pct = 8 },
|
.etf_metrics => .{ .seconds = Ttl.etf_metrics, .jitter_pct = 8 },
|
||||||
.entity_facts => .{ .seconds = Ttl.entity_facts, .jitter_pct = 8 },
|
.entity_facts => .{ .seconds = Ttl.entity_facts, .jitter_pct = 8 },
|
||||||
|
|
@ -292,7 +284,6 @@ pub const Store = struct {
|
||||||
Split => .splits,
|
Split => .splits,
|
||||||
EarningsEvent => .earnings,
|
EarningsEvent => .earnings,
|
||||||
OptionsChain => .options,
|
OptionsChain => .options,
|
||||||
EtfProfile => .etf_profile,
|
|
||||||
Wikidata.ClassificationRecord => .classification,
|
Wikidata.ClassificationRecord => .classification,
|
||||||
Edgar.EtfMetricRecord => .etf_metrics,
|
Edgar.EtfMetricRecord => .etf_metrics,
|
||||||
Edgar.EntityFactRecord => .entity_facts,
|
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 {
|
pub fn DataFor(comptime T: type) type {
|
||||||
return if (T == EtfProfile) EtfProfile else []T;
|
return []T;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn CacheResult(comptime T: type) type {
|
pub fn CacheResult(comptime T: type) type {
|
||||||
|
|
@ -345,15 +337,12 @@ pub const Store = struct {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (T == EtfProfile or T == OptionsChain) {
|
if (T == OptionsChain) {
|
||||||
const is_negative = std.mem.eql(u8, data, negative_cache_content);
|
const is_negative = std.mem.eql(u8, data, negative_cache_content);
|
||||||
if (is_negative) {
|
if (is_negative) {
|
||||||
if (freshness == .fresh_only) {
|
if (freshness == .fresh_only) {
|
||||||
// Negative entries are always fresh — return empty data
|
// Negative entries are always fresh — return empty data
|
||||||
if (T == EtfProfile)
|
return .{ .data = &.{}, .timestamp = std.Io.Timestamp.now(self.io, .real).toSeconds() };
|
||||||
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 null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -368,21 +357,14 @@ pub const Store = struct {
|
||||||
}
|
}
|
||||||
const timestamp = it.created orelse std.Io.Timestamp.now(self.io, .real).toSeconds();
|
const timestamp = it.created orelse std.Io.Timestamp.now(self.io, .real).toSeconds();
|
||||||
|
|
||||||
if (T == EtfProfile) {
|
const items = deserializeOptions(allocator, &it) catch return null;
|
||||||
const profile = deserializeEtfProfile(allocator, &it) catch return null;
|
return .{ .data = items, .timestamp = timestamp };
|
||||||
return .{ .data = profile, .timestamp = timestamp };
|
|
||||||
}
|
|
||||||
if (T == OptionsChain) {
|
|
||||||
const items = deserializeOptions(allocator, &it) catch return null;
|
|
||||||
return .{ .data = items, .timestamp = timestamp };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return readSlice(T, self.io, allocator, data, postProcess, freshness);
|
return readSlice(T, self.io, allocator, data, postProcess, freshness);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize data and write to cache with the given TTL.
|
/// 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`,
|
/// For `Dividend` and `Split`, this dispatches to `writeMerged`,
|
||||||
/// which performs sorted-union-with-existing semantics rather than
|
/// 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 expires = computeExpires(std.Io.Timestamp.now(self.io, .real).toSeconds(), ttl, symbol);
|
||||||
const data_type = dataTypeFor(T);
|
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) {
|
if (T == OptionsChain) {
|
||||||
const srf_data = serializeOptions(self.io, self.allocator, items, .{ .expires = expires }) catch |err| {
|
const srf_data = serializeOptions(self.io, self.allocator, items, .{ .expires = expires }) catch |err| {
|
||||||
log.warn("{s}: failed to serialize options: {s}", .{ symbol, @errorName(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));
|
try std.testing.expect(!hasNoStringFields(EarningsEvent));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "hasNoStringFields: EtfProfile has string fields -> false" {
|
|
||||||
try std.testing.expect(!hasNoStringFields(EtfProfile));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "hasNoStringFields: synthetic shapes" {
|
test "hasNoStringFields: synthetic shapes" {
|
||||||
// Pure ints/floats/bools/enums + Date — should pass.
|
// Pure ints/floats/bools/enums + Date — should pass.
|
||||||
const Pure = struct {
|
const Pure = struct {
|
||||||
|
|
@ -1759,88 +1726,6 @@ pub const Store = struct {
|
||||||
|
|
||||||
return chains.toOwnedSlice(allocator);
|
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.
|
/// Serialize a portfolio (list of lots) to SRF format.
|
||||||
|
|
@ -2741,9 +2626,8 @@ test "TTL constants are reasonable" {
|
||||||
// Options refresh hourly
|
// Options refresh hourly
|
||||||
try std.testing.expectEqual(@as(i64, std.time.s_per_hour), Ttl.options);
|
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.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
|
// New types: classification (90d) and etf_metrics (90d) refresh
|
||||||
// quarterly; entity_facts (30d) refreshes monthly.
|
// 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(Ttl.tickers_companies, tc.seconds);
|
||||||
try std.testing.expectEqual(@as(u8, 8), tc.jitter_pct);
|
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(Ttl.options, DataType.options.ttl().seconds);
|
||||||
try std.testing.expectEqual(@as(u8, 0), DataType.options.ttl().jitter_pct);
|
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(Ttl.earnings, DataType.earnings.ttl().seconds);
|
||||||
try std.testing.expectEqual(@as(u8, 0), DataType.earnings.ttl().jitter_pct);
|
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
|
// candles_daily, candles_meta, and meta have their own writers
|
||||||
// (`cacheCandles`, `writeNegative`); calling .ttl() on them is
|
// (`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("splits.srf", DataType.splits.fileName());
|
||||||
try std.testing.expectEqualStrings("options.srf", DataType.options.fileName());
|
try std.testing.expectEqualStrings("options.srf", DataType.options.fileName());
|
||||||
try std.testing.expectEqualStrings("earnings.srf", DataType.earnings.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("meta.srf", DataType.meta.fileName());
|
||||||
try std.testing.expectEqualStrings("classification.srf", DataType.classification.fileName());
|
try std.testing.expectEqualStrings("classification.srf", DataType.classification.fileName());
|
||||||
try std.testing.expectEqualStrings("etf_metrics.srf", DataType.etf_metrics.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.splits, Store.dataTypeFor(Split));
|
||||||
try std.testing.expectEqual(DataType.earnings, Store.dataTypeFor(EarningsEvent));
|
try std.testing.expectEqual(DataType.earnings, Store.dataTypeFor(EarningsEvent));
|
||||||
try std.testing.expectEqual(DataType.options, Store.dataTypeFor(OptionsChain));
|
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" {
|
test "Store.DataFor returns correct types" {
|
||||||
// EtfProfile returns single struct, others return slices
|
// Every supported type is cached as a slice of records.
|
||||||
try std.testing.expect(@TypeOf(Store.DataFor(EtfProfile)) == type);
|
|
||||||
try std.testing.expect(Store.DataFor(EtfProfile) == EtfProfile);
|
|
||||||
try std.testing.expect(Store.DataFor(Candle) == []Candle);
|
try std.testing.expect(Store.DataFor(Candle) == []Candle);
|
||||||
try std.testing.expect(Store.DataFor(Dividend) == []Dividend);
|
try std.testing.expect(Store.DataFor(Dividend) == []Dividend);
|
||||||
try std.testing.expect(Store.DataFor(Split) == []Split);
|
try std.testing.expect(Store.DataFor(Split) == []Split);
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ const display_types = [_]DataType{
|
||||||
.splits,
|
.splits,
|
||||||
.options,
|
.options,
|
||||||
.earnings,
|
.earnings,
|
||||||
.etf_profile,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const display_labels = [_][]const u8{
|
const display_labels = [_][]const u8{
|
||||||
|
|
@ -52,7 +51,6 @@ const display_labels = [_][]const u8{
|
||||||
"splits",
|
"splits",
|
||||||
"options",
|
"options",
|
||||||
"earnings",
|
"earnings",
|
||||||
"etf_profile",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
|
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,19 @@ pub const Source = enum {
|
||||||
fetched,
|
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.
|
/// Generic result type for all fetch operations: data payload + provenance metadata.
|
||||||
///
|
///
|
||||||
/// `data` is owned by `allocator` — call `result.deinit()` to release
|
/// `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.
|
/// allocator (e.g. an arena) differed from the service's allocator.
|
||||||
pub fn FetchResult(comptime T: type) type {
|
pub fn FetchResult(comptime T: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
data: cache.Store.DataFor(T),
|
data: PayloadFor(T),
|
||||||
source: Source,
|
source: Source,
|
||||||
timestamp: i64,
|
timestamp: i64,
|
||||||
/// Allocator that owns `data`. Populated by the service on
|
/// 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
|
// No proactive token-bucket limiter for these. Tiingo
|
||||||
// (candles) has a 1000/day quota enforced reactively
|
// (candles) has a 1000/day quota enforced reactively
|
||||||
// via 429-then-backoff in `getCandles`; Wikidata
|
// via 429-then-backoff in `getCandles`; Wikidata
|
||||||
// (classification) has no published quota; the legacy
|
// (classification) has no published quota; the `meta`
|
||||||
// `etf_profile` and `meta` types aren't fetched. Nothing
|
// type isn't fetched. Nothing useful to wait for at the
|
||||||
// useful to wait for at the call site, so report 0.
|
// call site, so report 0.
|
||||||
.candles_daily, .candles_meta, .classification, .etf_profile, .meta => 0,
|
.candles_daily, .candles_meta, .classification, .meta => 0,
|
||||||
};
|
};
|
||||||
return if (ns == 0) 0 else @max(1, ns / std.time.ns_per_s);
|
return if (ns == 0) 0 else @max(1, ns / std.time.ns_per_s);
|
||||||
}
|
}
|
||||||
|
|
@ -2759,7 +2772,6 @@ pub const DataService = struct {
|
||||||
.earnings => "/earnings",
|
.earnings => "/earnings",
|
||||||
.options => "/options",
|
.options => "/options",
|
||||||
.splits => "/splits",
|
.splits => "/splits",
|
||||||
.etf_profile => return false, // not served (replaced by etf_metrics)
|
|
||||||
.meta => return false,
|
.meta => return false,
|
||||||
.classification => "/classification",
|
.classification => "/classification",
|
||||||
.etf_metrics => "/etf_metrics",
|
.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_daily));
|
||||||
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.candles_meta));
|
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(.classification));
|
||||||
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.etf_profile));
|
|
||||||
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.meta));
|
try std.testing.expectEqual(@as(?u64, 0), svc.estimateWaitSeconds(.meta));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue