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 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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue