move to srf iterator/bail early when possible
This commit is contained in:
parent
0a2dd47f3e
commit
44dfafd574
6 changed files with 62 additions and 65 deletions
|
|
@ -13,8 +13,8 @@
|
||||||
.hash = "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ",
|
.hash = "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ",
|
||||||
},
|
},
|
||||||
.srf = .{
|
.srf = .{
|
||||||
.url = "git+https://git.lerch.org/lobo/srf.git#95036e83e26bb885641c62aaf1e26dbfbb147ea9",
|
.url = "git+https://git.lerch.org/lobo/srf.git#8e12b7396afc1bcbc4e2a3f19d8725a82b71b27e",
|
||||||
.hash = "srf-0.0.0-qZj575xZAQB4wzO6J8wf0hBFTZMDjCfFFCtHx6BCQifK",
|
.hash = "srf-0.0.0-qZj573V9AQBJTR8ehcnA6KW_wb6cdkJZtFZGq87b8dAJ",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
.paths = .{
|
.paths = .{
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ pub fn parseAccountsFile(allocator: std.mem.Allocator, data: []const u8) !Accoun
|
||||||
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
||||||
defer parsed.deinit();
|
defer parsed.deinit();
|
||||||
|
|
||||||
for (parsed.records.items) |record| {
|
for (parsed.records) |record| {
|
||||||
const entry = record.to(AccountTaxEntry) catch continue;
|
const entry = record.to(AccountTaxEntry) catch continue;
|
||||||
try entries.append(allocator, .{
|
try entries.append(allocator, .{
|
||||||
.account = try allocator.dupe(u8, entry.account),
|
.account = try allocator.dupe(u8, entry.account),
|
||||||
|
|
|
||||||
115
src/cache/store.zig
vendored
115
src/cache/store.zig
vendored
|
|
@ -129,18 +129,19 @@ pub const Store = struct {
|
||||||
/// - Negative cache entries (# fetch_failed) are always fresh.
|
/// - Negative cache entries (# fetch_failed) are always fresh.
|
||||||
/// - Data with `#!expires=` is fresh if the SRF library says so.
|
/// - Data with `#!expires=` is fresh if the SRF library says so.
|
||||||
/// - Data without expiry metadata is considered stale (triggers re-fetch).
|
/// - Data without expiry metadata is considered stale (triggers re-fetch).
|
||||||
|
/// Uses the SRF iterator to read only the header directives without parsing any records.
|
||||||
pub fn isFreshData(data: []const u8, allocator: std.mem.Allocator) bool {
|
pub fn isFreshData(data: []const u8, allocator: std.mem.Allocator) bool {
|
||||||
// Negative cache entry -- always fresh
|
// Negative cache entry -- always fresh
|
||||||
if (std.mem.indexOf(u8, data, "# fetch_failed")) |_| return true;
|
if (std.mem.indexOf(u8, data, "# fetch_failed")) |_| return true;
|
||||||
|
|
||||||
var reader = std.Io.Reader.fixed(data);
|
var reader = std.Io.Reader.fixed(data);
|
||||||
const parsed = srf.parse(&reader, allocator, .{}) catch return false;
|
const it = srf.iterator(&reader, allocator, .{}) catch return false;
|
||||||
defer parsed.deinit();
|
defer it.deinit();
|
||||||
|
|
||||||
// No expiry directive → stale (legacy file, trigger re-fetch + rewrite)
|
// No expiry directive → stale (legacy file, trigger re-fetch + rewrite)
|
||||||
if (parsed.expires == null) return false;
|
if (it.expires == null) return false;
|
||||||
|
|
||||||
return parsed.isFresh();
|
return it.isFresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear all cached data for a symbol.
|
/// Clear all cached data for a symbol.
|
||||||
|
|
@ -232,11 +233,11 @@ pub const Store = struct {
|
||||||
errdefer candles.deinit(allocator);
|
errdefer candles.deinit(allocator);
|
||||||
|
|
||||||
var reader = std.Io.Reader.fixed(data);
|
var reader = std.Io.Reader.fixed(data);
|
||||||
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
||||||
defer parsed.deinit();
|
defer it.deinit();
|
||||||
|
|
||||||
for (parsed.records.items) |record| {
|
while (try it.next()) |fields| {
|
||||||
const candle = record.to(Candle) catch continue;
|
const candle = fields.to(Candle) catch continue;
|
||||||
try candles.append(allocator, candle);
|
try candles.append(allocator, candle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -262,13 +263,14 @@ pub const Store = struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deserialize candle metadata from SRF data.
|
/// Deserialize candle metadata from SRF data.
|
||||||
|
/// Uses the SRF iterator to read only the first record without parsing the entire file.
|
||||||
pub fn deserializeCandleMeta(allocator: std.mem.Allocator, data: []const u8) !CandleMeta {
|
pub fn deserializeCandleMeta(allocator: std.mem.Allocator, data: []const u8) !CandleMeta {
|
||||||
var reader = std.Io.Reader.fixed(data);
|
var reader = std.Io.Reader.fixed(data);
|
||||||
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
||||||
defer parsed.deinit();
|
defer it.deinit();
|
||||||
|
|
||||||
if (parsed.records.items.len == 0) return error.InvalidData;
|
const fields = (try it.next()) orelse return error.InvalidData;
|
||||||
return parsed.records.items[0].to(CandleMeta) catch error.InvalidData;
|
return fields.to(CandleMeta) catch error.InvalidData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inline fetch metadata embedded as the first record in non-candle SRF files.
|
/// Inline fetch metadata embedded as the first record in non-candle SRF files.
|
||||||
|
|
@ -278,14 +280,15 @@ pub const Store = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Read the `fetched_at` timestamp from the first record of an SRF file.
|
/// Read the `fetched_at` timestamp from the first record of an SRF file.
|
||||||
|
/// Uses the SRF iterator to read only the first record without parsing the entire file.
|
||||||
/// Returns null if the file has no FetchMeta record or cannot be parsed.
|
/// Returns null if the file has no FetchMeta record or cannot be parsed.
|
||||||
pub fn readFetchedAt(allocator: std.mem.Allocator, data: []const u8) ?i64 {
|
pub fn readFetchedAt(allocator: std.mem.Allocator, data: []const u8) ?i64 {
|
||||||
var reader = std.Io.Reader.fixed(data);
|
var reader = std.Io.Reader.fixed(data);
|
||||||
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return null;
|
var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return null;
|
||||||
defer parsed.deinit();
|
defer it.deinit();
|
||||||
|
|
||||||
if (parsed.records.items.len == 0) return null;
|
const fields = (it.next() catch return null) orelse return null;
|
||||||
const meta = parsed.records.items[0].to(FetchMeta) catch return null;
|
const meta = fields.to(FetchMeta) catch return null;
|
||||||
return meta.fetched_at;
|
return meta.fetched_at;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,12 +326,12 @@ pub const Store = struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var reader = std.Io.Reader.fixed(data);
|
var reader = std.Io.Reader.fixed(data);
|
||||||
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
||||||
defer parsed.deinit();
|
defer it.deinit();
|
||||||
|
|
||||||
for (parsed.records.items) |record| {
|
while (try it.next()) |fields| {
|
||||||
var div = record.to(Dividend) catch continue;
|
var div = fields.to(Dividend) catch continue;
|
||||||
// Dupe owned strings before parsed.deinit() frees the backing buffer
|
// Dupe owned strings before iterator.deinit() frees the backing buffer
|
||||||
if (div.currency) |c| {
|
if (div.currency) |c| {
|
||||||
div.currency = allocator.dupe(u8, c) catch null;
|
div.currency = allocator.dupe(u8, c) catch null;
|
||||||
}
|
}
|
||||||
|
|
@ -344,11 +347,11 @@ pub const Store = struct {
|
||||||
errdefer splits.deinit(allocator);
|
errdefer splits.deinit(allocator);
|
||||||
|
|
||||||
var reader = std.Io.Reader.fixed(data);
|
var reader = std.Io.Reader.fixed(data);
|
||||||
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
||||||
defer parsed.deinit();
|
defer it.deinit();
|
||||||
|
|
||||||
for (parsed.records.items) |record| {
|
while (try it.next()) |fields| {
|
||||||
const split = record.to(Split) catch continue;
|
const split = fields.to(Split) catch continue;
|
||||||
try splits.append(allocator, split);
|
try splits.append(allocator, split);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -373,11 +376,11 @@ pub const Store = struct {
|
||||||
errdefer events.deinit(allocator);
|
errdefer events.deinit(allocator);
|
||||||
|
|
||||||
var reader = std.Io.Reader.fixed(data);
|
var reader = std.Io.Reader.fixed(data);
|
||||||
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
||||||
defer parsed.deinit();
|
defer it.deinit();
|
||||||
|
|
||||||
for (parsed.records.items) |record| {
|
while (try it.next()) |fields| {
|
||||||
var ev = record.to(EarningsEvent) catch continue;
|
var ev = fields.to(EarningsEvent) catch continue;
|
||||||
// Recompute surprise from actual/estimate
|
// Recompute surprise from actual/estimate
|
||||||
if (ev.actual != null and ev.estimate != null) {
|
if (ev.actual != null and ev.estimate != null) {
|
||||||
ev.surprise = ev.actual.? - ev.estimate.?;
|
ev.surprise = ev.actual.? - ev.estimate.?;
|
||||||
|
|
@ -444,8 +447,8 @@ pub const Store = struct {
|
||||||
/// Deserialize ETF profile from SRF data.
|
/// Deserialize ETF profile from SRF data.
|
||||||
pub fn deserializeEtfProfile(allocator: std.mem.Allocator, data: []const u8) !EtfProfile {
|
pub fn deserializeEtfProfile(allocator: std.mem.Allocator, data: []const u8) !EtfProfile {
|
||||||
var reader = std.Io.Reader.fixed(data);
|
var reader = std.Io.Reader.fixed(data);
|
||||||
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
||||||
defer parsed.deinit();
|
defer it.deinit();
|
||||||
|
|
||||||
var profile = EtfProfile{ .symbol = "" };
|
var profile = EtfProfile{ .symbol = "" };
|
||||||
var sectors: std.ArrayList(SectorWeight) = .empty;
|
var sectors: std.ArrayList(SectorWeight) = .empty;
|
||||||
|
|
@ -453,8 +456,8 @@ pub const Store = struct {
|
||||||
var holdings: std.ArrayList(Holding) = .empty;
|
var holdings: std.ArrayList(Holding) = .empty;
|
||||||
errdefer holdings.deinit(allocator);
|
errdefer holdings.deinit(allocator);
|
||||||
|
|
||||||
for (parsed.records.items) |record| {
|
while (try it.next()) |fields| {
|
||||||
const etf_rec = record.to(EtfRecord) catch continue;
|
const etf_rec = fields.to(EtfRecord) catch continue;
|
||||||
switch (etf_rec) {
|
switch (etf_rec) {
|
||||||
.meta => |m| {
|
.meta => |m| {
|
||||||
profile.expense_ratio = m.expense_ratio;
|
profile.expense_ratio = m.expense_ratio;
|
||||||
|
|
@ -583,10 +586,12 @@ pub const Store = struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deserialize options chains from SRF data.
|
/// Deserialize options chains from SRF data.
|
||||||
|
/// Chain headers appear before their contracts in the SRF file, so a single
|
||||||
|
/// pass can assign contracts to the correct chain as they are encountered.
|
||||||
pub fn deserializeOptions(allocator: std.mem.Allocator, data: []const u8) ![]OptionsChain {
|
pub fn deserializeOptions(allocator: std.mem.Allocator, data: []const u8) ![]OptionsChain {
|
||||||
var reader = std.Io.Reader.fixed(data);
|
var reader = std.Io.Reader.fixed(data);
|
||||||
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
||||||
defer parsed.deinit();
|
defer it.deinit();
|
||||||
|
|
||||||
var chains: std.ArrayList(OptionsChain) = .empty;
|
var chains: std.ArrayList(OptionsChain) = .empty;
|
||||||
errdefer {
|
errdefer {
|
||||||
|
|
@ -601,26 +606,7 @@ pub const Store = struct {
|
||||||
var exp_map = std.AutoHashMap(i32, usize).init(allocator);
|
var exp_map = std.AutoHashMap(i32, usize).init(allocator);
|
||||||
defer exp_map.deinit();
|
defer exp_map.deinit();
|
||||||
|
|
||||||
// First pass: collect chain headers
|
// Accumulate contracts per chain
|
||||||
for (parsed.records.items) |record| {
|
|
||||||
const opt_rec = record.to(OptionsRecord) catch continue;
|
|
||||||
switch (opt_rec) {
|
|
||||||
.chain => |ch| {
|
|
||||||
const idx = chains.items.len;
|
|
||||||
try chains.append(allocator, .{
|
|
||||||
.underlying_symbol = try allocator.dupe(u8, ch.symbol),
|
|
||||||
.underlying_price = ch.price,
|
|
||||||
.expiration = ch.expiration,
|
|
||||||
.calls = &.{},
|
|
||||||
.puts = &.{},
|
|
||||||
});
|
|
||||||
try exp_map.put(ch.expiration.days, idx);
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second pass: collect contracts
|
|
||||||
var calls_map = std.AutoHashMap(usize, std.ArrayList(OptionContract)).init(allocator);
|
var calls_map = std.AutoHashMap(usize, std.ArrayList(OptionContract)).init(allocator);
|
||||||
defer {
|
defer {
|
||||||
var iter = calls_map.valueIterator();
|
var iter = calls_map.valueIterator();
|
||||||
|
|
@ -634,9 +620,21 @@ pub const Store = struct {
|
||||||
puts_map.deinit();
|
puts_map.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (parsed.records.items) |record| {
|
// Single pass: chain headers and contracts arrive in order
|
||||||
const opt_rec = record.to(OptionsRecord) catch continue;
|
while (try it.next()) |fields| {
|
||||||
|
const opt_rec = fields.to(OptionsRecord) catch continue;
|
||||||
switch (opt_rec) {
|
switch (opt_rec) {
|
||||||
|
.chain => |ch| {
|
||||||
|
const idx = chains.items.len;
|
||||||
|
try chains.append(allocator, .{
|
||||||
|
.underlying_symbol = try allocator.dupe(u8, ch.symbol),
|
||||||
|
.underlying_price = ch.price,
|
||||||
|
.expiration = ch.expiration,
|
||||||
|
.calls = &.{},
|
||||||
|
.puts = &.{},
|
||||||
|
});
|
||||||
|
try exp_map.put(ch.expiration.days, idx);
|
||||||
|
},
|
||||||
.call => |cf| {
|
.call => |cf| {
|
||||||
if (exp_map.get(cf.expiration.days)) |idx| {
|
if (exp_map.get(cf.expiration.days)) |idx| {
|
||||||
const entry = try calls_map.getOrPut(idx);
|
const entry = try calls_map.getOrPut(idx);
|
||||||
|
|
@ -651,7 +649,6 @@ pub const Store = struct {
|
||||||
try entry.value_ptr.append(allocator, fieldsToContract(cf, .put));
|
try entry.value_ptr.append(allocator, fieldsToContract(cf, .put));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.chain => {},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -712,7 +709,7 @@ pub fn deserializePortfolio(allocator: std.mem.Allocator, data: []const u8) !Por
|
||||||
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
||||||
defer parsed.deinit();
|
defer parsed.deinit();
|
||||||
|
|
||||||
for (parsed.records.items) |record| {
|
for (parsed.records) |record| {
|
||||||
var lot = Lot{
|
var lot = Lot{
|
||||||
.symbol = "",
|
.symbol = "",
|
||||||
.shares = 0,
|
.shares = 0,
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ pub fn parseClassificationFile(allocator: std.mem.Allocator, data: []const u8) !
|
||||||
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
const parsed = srf.parse(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
||||||
defer parsed.deinit();
|
defer parsed.deinit();
|
||||||
|
|
||||||
for (parsed.records.items) |record| {
|
for (parsed.records) |record| {
|
||||||
const entry = record.to(ClassificationEntry) catch continue;
|
const entry = record.to(ClassificationEntry) catch continue;
|
||||||
try entries.append(allocator, .{
|
try entries.append(allocator, .{
|
||||||
.symbol = try allocator.dupe(u8, entry.symbol),
|
.symbol = try allocator.dupe(u8, entry.symbol),
|
||||||
|
|
|
||||||
|
|
@ -321,7 +321,7 @@ pub fn loadFromData(allocator: std.mem.Allocator, data: []const u8) ?KeyMap {
|
||||||
|
|
||||||
var bindings = std.ArrayList(Binding).empty;
|
var bindings = std.ArrayList(Binding).empty;
|
||||||
|
|
||||||
for (parsed.records.items) |record| {
|
for (parsed.records) |record| {
|
||||||
var action: ?Action = null;
|
var action: ?Action = null;
|
||||||
var key: ?KeyCombo = null;
|
var key: ?KeyCombo = null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,7 @@ pub fn loadFromData(data: []const u8) ?Theme {
|
||||||
|
|
||||||
var theme = default_theme;
|
var theme = default_theme;
|
||||||
|
|
||||||
for (parsed.records.items) |record| {
|
for (parsed.records) |record| {
|
||||||
for (record.fields) |field| {
|
for (record.fields) |field| {
|
||||||
if (field.value) |v| {
|
if (field.value) |v| {
|
||||||
const str = switch (v) {
|
const str = switch (v) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue