ai: remove most manual type conversion in favor of srf
This commit is contained in:
parent
c65ebb9384
commit
57b0811ecd
7 changed files with 233 additions and 506 deletions
|
|
@ -13,8 +13,8 @@
|
|||
.hash = "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ",
|
||||
},
|
||||
.srf = .{
|
||||
.url = "git+https://git.lerch.org/lobo/srf.git#18a4558b47acb3d62701351820cd70dcc2c56c5a",
|
||||
.hash = "srf-0.0.0-qZj579ZBAQCJdGtTnPQlxlGPw3g2gmTmwer8V6yyo-ga",
|
||||
.url = "git+https://git.lerch.org/lobo/srf.git#7aa7ec112af736490ce7b5a887886a1da1212c6a",
|
||||
.hash = "srf-0.0.0-qZj57-ZRAQARvv95mwaqA_39lAEKbuhQdUdmSQ5qhei5",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
|
|
|
|||
662
src/cache/store.zig
vendored
662
src/cache/store.zig
vendored
|
|
@ -14,6 +14,7 @@ const Lot = @import("../models/portfolio.zig").Lot;
|
|||
const Portfolio = @import("../models/portfolio.zig").Portfolio;
|
||||
const OptionsChain = @import("../models/option.zig").OptionsChain;
|
||||
const OptionContract = @import("../models/option.zig").OptionContract;
|
||||
const ContractType = @import("../models/option.zig").ContractType;
|
||||
|
||||
/// TTL durations in seconds for cache expiry.
|
||||
pub const Ttl = struct {
|
||||
|
|
@ -95,32 +96,7 @@ pub const Store = struct {
|
|||
};
|
||||
}
|
||||
|
||||
/// Write raw SRF data for a symbol and data type with an embedded expiry timestamp.
|
||||
/// Inserts a `# good_until::{unix_seconds}` comment after the `#!srfv1` header so
|
||||
/// freshness can be determined from the file content rather than filesystem mtime.
|
||||
pub fn writeWithExpiry(self: *Store, symbol: []const u8, data_type: DataType, data: []const u8, ttl_seconds: i64) !void {
|
||||
try self.ensureSymbolDir(symbol);
|
||||
const path = try self.symbolPath(symbol, data_type.fileName());
|
||||
defer self.allocator.free(path);
|
||||
|
||||
const file = try std.fs.cwd().createFile(path, .{});
|
||||
defer file.close();
|
||||
|
||||
const header = "#!srfv1\n";
|
||||
if (std.mem.startsWith(u8, data, header)) {
|
||||
try file.writeAll(header);
|
||||
var expiry_buf: [48]u8 = undefined;
|
||||
const expiry = std.time.timestamp() + ttl_seconds;
|
||||
const expiry_line = std.fmt.bufPrint(&expiry_buf, "# good_until::{d}\n", .{expiry}) catch return;
|
||||
try file.writeAll(expiry_line);
|
||||
try file.writeAll(data[header.len..]);
|
||||
} else {
|
||||
// Unexpected format -- write as-is to avoid data loss
|
||||
try file.writeAll(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Write raw SRF data for a symbol and data type (no expiry metadata).
|
||||
/// Write raw SRF data for a symbol and data type.
|
||||
pub fn writeRaw(self: *Store, symbol: []const u8, data_type: DataType, data: []const u8) !void {
|
||||
try self.ensureSymbolDir(symbol);
|
||||
const path = try self.symbolPath(symbol, data_type.fileName());
|
||||
|
|
@ -131,35 +107,22 @@ pub const Store = struct {
|
|||
try file.writeAll(data);
|
||||
}
|
||||
|
||||
/// Check if a cached data file is fresh by reading the embedded expiry timestamp.
|
||||
/// Check if raw SRF data is fresh using the embedded `#!expires=` directive.
|
||||
/// - Negative cache entries (# fetch_failed) are always fresh.
|
||||
/// - Files with `# good_until::{timestamp}` are fresh if now < timestamp.
|
||||
/// - Files without expiry metadata are considered stale (triggers re-fetch + rewrite).
|
||||
pub fn isFresh(self: *Store, symbol: []const u8, data_type: DataType) !bool {
|
||||
const path = try self.symbolPath(symbol, data_type.fileName());
|
||||
defer self.allocator.free(path);
|
||||
|
||||
const file = std.fs.cwd().openFile(path, .{}) catch return false;
|
||||
defer file.close();
|
||||
|
||||
// Read enough to find the good_until or fetch_failed comment lines
|
||||
var buf: [128]u8 = undefined;
|
||||
const n = file.readAll(&buf) catch return false;
|
||||
const content = buf[0..n];
|
||||
|
||||
/// - Data with `#!expires=` is fresh if the SRF library says so.
|
||||
/// - Data without expiry metadata is considered stale (triggers re-fetch).
|
||||
pub fn isFreshData(data: []const u8, allocator: std.mem.Allocator) bool {
|
||||
// Negative cache entry -- always fresh
|
||||
if (std.mem.indexOf(u8, content, "# fetch_failed")) |_| return true;
|
||||
if (std.mem.indexOf(u8, data, "# fetch_failed")) |_| return true;
|
||||
|
||||
// Look for embedded expiry
|
||||
if (std.mem.indexOf(u8, content, "# good_until::")) |idx| {
|
||||
const after = content[idx + "# good_until::".len ..];
|
||||
const end = std.mem.indexOfScalar(u8, after, '\n') orelse after.len;
|
||||
const expiry = std.fmt.parseInt(i64, after[0..end], 10) catch return false;
|
||||
return std.time.timestamp() < expiry;
|
||||
}
|
||||
var reader = std.Io.Reader.fixed(data);
|
||||
const parsed = srf.parse(&reader, allocator, .{}) catch return false;
|
||||
defer parsed.deinit();
|
||||
|
||||
// No expiry info (legacy file or missing metadata) -- stale
|
||||
return false;
|
||||
// No expiry directive → stale (legacy file, trigger re-fetch + rewrite)
|
||||
if (parsed.expires == null) return false;
|
||||
|
||||
return parsed.isFresh();
|
||||
}
|
||||
|
||||
/// Get the modification time (unix seconds) of a cached data file.
|
||||
|
|
@ -259,11 +222,11 @@ pub const Store = struct {
|
|||
// -- Serialization helpers --
|
||||
|
||||
/// Serialize candles to SRF compact format.
|
||||
pub fn serializeCandles(allocator: std.mem.Allocator, candles: []const Candle) ![]const u8 {
|
||||
pub fn serializeCandles(allocator: std.mem.Allocator, candles: []const Candle, options: srf.FormatOptions) ![]const u8 {
|
||||
var buf: std.ArrayList(u8) = .empty;
|
||||
errdefer buf.deinit(allocator);
|
||||
const writer = buf.writer(allocator);
|
||||
try writer.print("{f}", .{srf.fmtFrom(Candle, allocator, candles, .{})});
|
||||
try writer.print("{f}", .{srf.fmtFrom(Candle, allocator, candles, options)});
|
||||
return buf.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
|
|
@ -277,38 +240,7 @@ pub const Store = struct {
|
|||
defer parsed.deinit();
|
||||
|
||||
for (parsed.records.items) |record| {
|
||||
var candle = Candle{
|
||||
.date = Date.epoch,
|
||||
.open = 0,
|
||||
.high = 0,
|
||||
.low = 0,
|
||||
.close = 0,
|
||||
.adj_close = 0,
|
||||
.volume = 0,
|
||||
};
|
||||
for (record.fields) |field| {
|
||||
if (std.mem.eql(u8, field.key, "date")) {
|
||||
if (field.value) |v| {
|
||||
const str = switch (v) {
|
||||
.string => |s| s,
|
||||
else => continue,
|
||||
};
|
||||
candle.date = Date.parse(str) catch continue;
|
||||
}
|
||||
} else if (std.mem.eql(u8, field.key, "open")) {
|
||||
if (field.value) |v| candle.open = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "high")) {
|
||||
if (field.value) |v| candle.high = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "low")) {
|
||||
if (field.value) |v| candle.low = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "close")) {
|
||||
if (field.value) |v| candle.close = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "adj_close")) {
|
||||
if (field.value) |v| candle.adj_close = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "volume")) {
|
||||
if (field.value) |v| candle.volume = @intFromFloat(numVal(v));
|
||||
}
|
||||
}
|
||||
const candle = record.to(Candle) catch continue;
|
||||
try candles.append(allocator, candle);
|
||||
}
|
||||
|
||||
|
|
@ -316,20 +248,20 @@ pub const Store = struct {
|
|||
}
|
||||
|
||||
/// Serialize dividends to SRF compact format.
|
||||
pub fn serializeDividends(allocator: std.mem.Allocator, dividends: []const Dividend) ![]const u8 {
|
||||
pub fn serializeDividends(allocator: std.mem.Allocator, dividends: []const Dividend, options: srf.FormatOptions) ![]const u8 {
|
||||
var buf: std.ArrayList(u8) = .empty;
|
||||
errdefer buf.deinit(allocator);
|
||||
const writer = buf.writer(allocator);
|
||||
try writer.print("{f}", .{srf.fmtFrom(Dividend, allocator, dividends, .{})});
|
||||
try writer.print("{f}", .{srf.fmtFrom(Dividend, allocator, dividends, options)});
|
||||
return buf.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
/// Serialize splits to SRF compact format.
|
||||
pub fn serializeSplits(allocator: std.mem.Allocator, splits: []const Split) ![]const u8 {
|
||||
pub fn serializeSplits(allocator: std.mem.Allocator, splits: []const Split, options: srf.FormatOptions) ![]const u8 {
|
||||
var buf: std.ArrayList(u8) = .empty;
|
||||
errdefer buf.deinit(allocator);
|
||||
const writer = buf.writer(allocator);
|
||||
try writer.print("{f}", .{srf.fmtFrom(Split, allocator, splits, .{})});
|
||||
try writer.print("{f}", .{srf.fmtFrom(Split, allocator, splits, options)});
|
||||
return buf.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
|
|
@ -343,44 +275,7 @@ pub const Store = struct {
|
|||
defer parsed.deinit();
|
||||
|
||||
for (parsed.records.items) |record| {
|
||||
var div = Dividend{
|
||||
.ex_date = Date.epoch,
|
||||
.amount = 0,
|
||||
};
|
||||
for (record.fields) |field| {
|
||||
if (std.mem.eql(u8, field.key, "ex_date")) {
|
||||
if (field.value) |v| {
|
||||
const str = switch (v) {
|
||||
.string => |s| s,
|
||||
else => continue,
|
||||
};
|
||||
div.ex_date = Date.parse(str) catch continue;
|
||||
}
|
||||
} else if (std.mem.eql(u8, field.key, "amount")) {
|
||||
if (field.value) |v| div.amount = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "pay_date")) {
|
||||
if (field.value) |v| {
|
||||
const str = switch (v) {
|
||||
.string => |s| s,
|
||||
else => continue,
|
||||
};
|
||||
div.pay_date = Date.parse(str) catch null;
|
||||
}
|
||||
} else if (std.mem.eql(u8, field.key, "frequency")) {
|
||||
if (field.value) |v| {
|
||||
const n = numVal(v);
|
||||
if (n > 0 and n <= 255) div.frequency = @intFromFloat(n);
|
||||
}
|
||||
} else if (std.mem.eql(u8, field.key, "type")) {
|
||||
if (field.value) |v| {
|
||||
const str = switch (v) {
|
||||
.string => |s| s,
|
||||
else => continue,
|
||||
};
|
||||
div.type = parseDividendTypeTag(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
const div = record.to(Dividend) catch continue;
|
||||
try dividends.append(allocator, div);
|
||||
}
|
||||
|
||||
|
|
@ -397,26 +292,7 @@ pub const Store = struct {
|
|||
defer parsed.deinit();
|
||||
|
||||
for (parsed.records.items) |record| {
|
||||
var split = Split{
|
||||
.date = Date.epoch,
|
||||
.numerator = 0,
|
||||
.denominator = 0,
|
||||
};
|
||||
for (record.fields) |field| {
|
||||
if (std.mem.eql(u8, field.key, "date")) {
|
||||
if (field.value) |v| {
|
||||
const str = switch (v) {
|
||||
.string => |s| s,
|
||||
else => continue,
|
||||
};
|
||||
split.date = Date.parse(str) catch continue;
|
||||
}
|
||||
} else if (std.mem.eql(u8, field.key, "numerator")) {
|
||||
if (field.value) |v| split.numerator = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "denominator")) {
|
||||
if (field.value) |v| split.denominator = numVal(v);
|
||||
}
|
||||
}
|
||||
const split = record.to(Split) catch continue;
|
||||
try splits.append(allocator, split);
|
||||
}
|
||||
|
||||
|
|
@ -424,11 +300,11 @@ pub const Store = struct {
|
|||
}
|
||||
|
||||
/// Serialize earnings events to SRF compact format.
|
||||
pub fn serializeEarnings(allocator: std.mem.Allocator, events: []const EarningsEvent) ![]const u8 {
|
||||
pub fn serializeEarnings(allocator: std.mem.Allocator, events: []const EarningsEvent, options: srf.FormatOptions) ![]const u8 {
|
||||
var buf: std.ArrayList(u8) = .empty;
|
||||
errdefer buf.deinit(allocator);
|
||||
const writer = buf.writer(allocator);
|
||||
try writer.print("{f}", .{srf.fmtFrom(EarningsEvent, allocator, events, .{})});
|
||||
try writer.print("{f}", .{srf.fmtFrom(EarningsEvent, allocator, events, options)});
|
||||
return buf.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
|
|
@ -442,47 +318,7 @@ pub const Store = struct {
|
|||
defer parsed.deinit();
|
||||
|
||||
for (parsed.records.items) |record| {
|
||||
var ev = EarningsEvent{
|
||||
.symbol = "",
|
||||
.date = Date.epoch,
|
||||
};
|
||||
for (record.fields) |field| {
|
||||
if (std.mem.eql(u8, field.key, "date")) {
|
||||
if (field.value) |v| {
|
||||
const str = switch (v) {
|
||||
.string => |s| s,
|
||||
else => continue,
|
||||
};
|
||||
ev.date = Date.parse(str) catch continue;
|
||||
}
|
||||
} else if (std.mem.eql(u8, field.key, "estimate")) {
|
||||
if (field.value) |v| ev.estimate = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "actual")) {
|
||||
if (field.value) |v| ev.actual = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "quarter")) {
|
||||
if (field.value) |v| {
|
||||
const n = numVal(v);
|
||||
if (n >= 1 and n <= 4) ev.quarter = @intFromFloat(n);
|
||||
}
|
||||
} else if (std.mem.eql(u8, field.key, "fiscal_year")) {
|
||||
if (field.value) |v| {
|
||||
const n = numVal(v);
|
||||
if (n > 1900 and n < 2200) ev.fiscal_year = @intFromFloat(n);
|
||||
}
|
||||
} else if (std.mem.eql(u8, field.key, "revenue_actual")) {
|
||||
if (field.value) |v| ev.revenue_actual = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "revenue_estimate")) {
|
||||
if (field.value) |v| ev.revenue_estimate = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "report_time")) {
|
||||
if (field.value) |v| {
|
||||
const str = switch (v) {
|
||||
.string => |s| s,
|
||||
else => continue,
|
||||
};
|
||||
ev.report_time = parseReportTimeTag(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
var ev = record.to(EarningsEvent) catch continue;
|
||||
// Recompute surprise from actual/estimate
|
||||
if (ev.actual != null and ev.estimate != null) {
|
||||
ev.surprise = ev.actual.? - ev.estimate.?;
|
||||
|
|
@ -496,45 +332,50 @@ pub const Store = struct {
|
|||
return events.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
/// SRF record types for ETF profile serialization.
|
||||
const EtfMeta = struct {
|
||||
expense_ratio: ?f64 = null,
|
||||
net_assets: ?f64 = null,
|
||||
dividend_yield: ?f64 = null,
|
||||
portfolio_turnover: ?f64 = null,
|
||||
total_holdings: ?u32 = null,
|
||||
inception_date: ?Date = null,
|
||||
leveraged: bool = false,
|
||||
};
|
||||
|
||||
const EtfRecord = union(enum) {
|
||||
pub const srf_tag_field = "type";
|
||||
meta: EtfMeta,
|
||||
sector: SectorWeight,
|
||||
holding: Holding,
|
||||
};
|
||||
|
||||
/// Serialize ETF profile to SRF compact format.
|
||||
/// Uses multiple record types: meta fields, then sector:: and holding:: prefixed records.
|
||||
pub fn serializeEtfProfile(allocator: std.mem.Allocator, profile: EtfProfile) ![]const u8 {
|
||||
/// Uses multiple record types: meta fields, then sector and holding records.
|
||||
pub fn serializeEtfProfile(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 = .{
|
||||
.expense_ratio = profile.expense_ratio,
|
||||
.net_assets = profile.net_assets,
|
||||
.dividend_yield = profile.dividend_yield,
|
||||
.portfolio_turnover = profile.portfolio_turnover,
|
||||
.total_holdings = profile.total_holdings,
|
||||
.inception_date = profile.inception_date,
|
||||
.leveraged = profile.leveraged,
|
||||
} });
|
||||
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 buf: std.ArrayList(u8) = .empty;
|
||||
errdefer buf.deinit(allocator);
|
||||
const writer = buf.writer(allocator);
|
||||
|
||||
try writer.writeAll("#!srfv1\n");
|
||||
|
||||
// Meta record
|
||||
try writer.writeAll("type::meta");
|
||||
if (profile.expense_ratio) |er| try writer.print(",expense_ratio:num:{d}", .{er});
|
||||
if (profile.net_assets) |na| try writer.print(",net_assets:num:{d}", .{na});
|
||||
if (profile.dividend_yield) |dy| try writer.print(",dividend_yield:num:{d}", .{dy});
|
||||
if (profile.portfolio_turnover) |pt| try writer.print(",portfolio_turnover:num:{d}", .{pt});
|
||||
if (profile.total_holdings) |th| try writer.print(",total_holdings:num:{d}", .{th});
|
||||
if (profile.inception_date) |d| {
|
||||
var db: [10]u8 = undefined;
|
||||
try writer.print(",inception_date::{s}", .{d.format(&db)});
|
||||
}
|
||||
if (profile.leveraged) try writer.writeAll(",leveraged::yes");
|
||||
try writer.writeAll("\n");
|
||||
|
||||
// Sector records
|
||||
if (profile.sectors) |sectors| {
|
||||
for (sectors) |sec| {
|
||||
try writer.print("type::sector,name::{s},weight:num:{d}\n", .{ sec.sector, sec.weight });
|
||||
}
|
||||
}
|
||||
|
||||
// Holding records
|
||||
if (profile.holdings) |holdings| {
|
||||
for (holdings) |h| {
|
||||
try writer.writeAll("type::holding");
|
||||
if (h.symbol) |s| try writer.print(",symbol::{s}", .{s});
|
||||
try writer.print(",name::{s},weight:num:{d}\n", .{ h.name, h.weight });
|
||||
}
|
||||
}
|
||||
|
||||
try writer.print("{f}", .{srf.fmtFrom(EtfRecord, allocator, records.items, options)});
|
||||
return buf.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
|
|
@ -551,92 +392,26 @@ pub const Store = struct {
|
|||
errdefer holdings.deinit(allocator);
|
||||
|
||||
for (parsed.records.items) |record| {
|
||||
var record_type: []const u8 = "";
|
||||
for (record.fields) |field| {
|
||||
if (std.mem.eql(u8, field.key, "type")) {
|
||||
if (field.value) |v| {
|
||||
record_type = switch (v) {
|
||||
.string => |s| s,
|
||||
else => "",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, record_type, "meta")) {
|
||||
for (record.fields) |field| {
|
||||
if (std.mem.eql(u8, field.key, "expense_ratio")) {
|
||||
if (field.value) |v| profile.expense_ratio = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "net_assets")) {
|
||||
if (field.value) |v| profile.net_assets = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "dividend_yield")) {
|
||||
if (field.value) |v| profile.dividend_yield = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "portfolio_turnover")) {
|
||||
if (field.value) |v| profile.portfolio_turnover = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "total_holdings")) {
|
||||
if (field.value) |v| {
|
||||
const n = numVal(v);
|
||||
if (n > 0) profile.total_holdings = @intFromFloat(n);
|
||||
}
|
||||
} else if (std.mem.eql(u8, field.key, "inception_date")) {
|
||||
if (field.value) |v| {
|
||||
const str = switch (v) {
|
||||
.string => |s| s,
|
||||
else => continue,
|
||||
};
|
||||
profile.inception_date = Date.parse(str) catch null;
|
||||
}
|
||||
} else if (std.mem.eql(u8, field.key, "leveraged")) {
|
||||
if (field.value) |v| {
|
||||
const str = switch (v) {
|
||||
.string => |s| s,
|
||||
else => continue,
|
||||
};
|
||||
profile.leveraged = std.mem.eql(u8, str, "yes");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (std.mem.eql(u8, record_type, "sector")) {
|
||||
var name: ?[]const u8 = null;
|
||||
var weight: f64 = 0;
|
||||
for (record.fields) |field| {
|
||||
if (std.mem.eql(u8, field.key, "name")) {
|
||||
if (field.value) |v| name = switch (v) {
|
||||
.string => |s| s,
|
||||
else => null,
|
||||
};
|
||||
} else if (std.mem.eql(u8, field.key, "weight")) {
|
||||
if (field.value) |v| weight = numVal(v);
|
||||
}
|
||||
}
|
||||
if (name) |n| {
|
||||
const duped = try allocator.dupe(u8, n);
|
||||
try sectors.append(allocator, .{ .sector = duped, .weight = weight });
|
||||
}
|
||||
} else if (std.mem.eql(u8, record_type, "holding")) {
|
||||
var sym: ?[]const u8 = null;
|
||||
var hname: ?[]const u8 = null;
|
||||
var weight: f64 = 0;
|
||||
for (record.fields) |field| {
|
||||
if (std.mem.eql(u8, field.key, "symbol")) {
|
||||
if (field.value) |v| sym = switch (v) {
|
||||
.string => |s| s,
|
||||
else => null,
|
||||
};
|
||||
} else if (std.mem.eql(u8, field.key, "name")) {
|
||||
if (field.value) |v| hname = switch (v) {
|
||||
.string => |s| s,
|
||||
else => null,
|
||||
};
|
||||
} else if (std.mem.eql(u8, field.key, "weight")) {
|
||||
if (field.value) |v| weight = numVal(v);
|
||||
}
|
||||
}
|
||||
if (hname) |hn| {
|
||||
const duped_sym = if (sym) |s| try allocator.dupe(u8, s) else null;
|
||||
const duped_name = try allocator.dupe(u8, hn);
|
||||
try holdings.append(allocator, .{ .symbol = duped_sym, .name = duped_name, .weight = weight });
|
||||
}
|
||||
const etf_rec = record.to(EtfRecord) catch continue;
|
||||
switch (etf_rec) {
|
||||
.meta => |m| {
|
||||
profile.expense_ratio = m.expense_ratio;
|
||||
profile.net_assets = m.net_assets;
|
||||
profile.dividend_yield = m.dividend_yield;
|
||||
profile.portfolio_turnover = m.portfolio_turnover;
|
||||
profile.total_holdings = m.total_holdings;
|
||||
profile.inception_date = m.inception_date;
|
||||
profile.leveraged = m.leveraged;
|
||||
},
|
||||
.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;
|
||||
const duped_name = try allocator.dupe(u8, h.name);
|
||||
try holdings.append(allocator, .{ .symbol = duped_sym, .name = duped_name, .weight = h.weight });
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -654,54 +429,91 @@ pub const Store = struct {
|
|||
return profile;
|
||||
}
|
||||
|
||||
/// SRF record types for options chain serialization.
|
||||
const ChainHeader = struct {
|
||||
expiration: Date,
|
||||
symbol: []const u8,
|
||||
price: ?f64 = null,
|
||||
};
|
||||
|
||||
const ContractFields = struct {
|
||||
expiration: Date,
|
||||
strike: f64,
|
||||
bid: ?f64 = null,
|
||||
ask: ?f64 = null,
|
||||
last: ?f64 = null,
|
||||
volume: ?u64 = null,
|
||||
oi: ?u64 = null,
|
||||
iv: ?f64 = null,
|
||||
delta: ?f64 = null,
|
||||
gamma: ?f64 = null,
|
||||
theta: ?f64 = null,
|
||||
vega: ?f64 = null,
|
||||
};
|
||||
|
||||
const OptionsRecord = union(enum) {
|
||||
pub const srf_tag_field = "type";
|
||||
chain: ChainHeader,
|
||||
call: ContractFields,
|
||||
put: ContractFields,
|
||||
};
|
||||
|
||||
fn contractToFields(c: OptionContract, expiration: Date) ContractFields {
|
||||
return .{
|
||||
.expiration = expiration,
|
||||
.strike = c.strike,
|
||||
.bid = c.bid,
|
||||
.ask = c.ask,
|
||||
.last = c.last_price,
|
||||
.volume = c.volume,
|
||||
.oi = c.open_interest,
|
||||
.iv = c.implied_volatility,
|
||||
.delta = c.delta,
|
||||
.gamma = c.gamma,
|
||||
.theta = c.theta,
|
||||
.vega = c.vega,
|
||||
};
|
||||
}
|
||||
|
||||
fn fieldsToContract(cf: ContractFields, contract_type: ContractType) OptionContract {
|
||||
return .{
|
||||
.contract_type = contract_type,
|
||||
.expiration = cf.expiration,
|
||||
.strike = cf.strike,
|
||||
.bid = cf.bid,
|
||||
.ask = cf.ask,
|
||||
.last_price = cf.last,
|
||||
.volume = cf.volume,
|
||||
.open_interest = cf.oi,
|
||||
.implied_volatility = cf.iv,
|
||||
.delta = cf.delta,
|
||||
.gamma = cf.gamma,
|
||||
.theta = cf.theta,
|
||||
.vega = cf.vega,
|
||||
};
|
||||
}
|
||||
|
||||
/// Serialize options chains to SRF compact format.
|
||||
pub fn serializeOptions(allocator: std.mem.Allocator, chains: []const OptionsChain) ![]const u8 {
|
||||
pub fn serializeOptions(allocator: std.mem.Allocator, chains: []const OptionsChain, options: srf.FormatOptions) ![]const u8 {
|
||||
var records: std.ArrayList(OptionsRecord) = .empty;
|
||||
defer records.deinit(allocator);
|
||||
|
||||
for (chains) |chain| {
|
||||
try records.append(allocator, .{ .chain = .{
|
||||
.expiration = chain.expiration,
|
||||
.symbol = chain.underlying_symbol,
|
||||
.price = chain.underlying_price,
|
||||
} });
|
||||
for (chain.calls) |c|
|
||||
try records.append(allocator, .{ .call = contractToFields(c, chain.expiration) });
|
||||
for (chain.puts) |p|
|
||||
try records.append(allocator, .{ .put = contractToFields(p, chain.expiration) });
|
||||
}
|
||||
|
||||
var buf: std.ArrayList(u8) = .empty;
|
||||
errdefer buf.deinit(allocator);
|
||||
const w = buf.writer(allocator);
|
||||
|
||||
try w.writeAll("#!srfv1\n");
|
||||
for (chains) |chain| {
|
||||
var exp_buf: [10]u8 = undefined;
|
||||
try w.print("type::chain,expiration::{s},symbol::{s}", .{
|
||||
chain.expiration.format(&exp_buf), chain.underlying_symbol,
|
||||
});
|
||||
if (chain.underlying_price) |p| try w.print(",price:num:{d}", .{p});
|
||||
try w.writeAll("\n");
|
||||
|
||||
for (chain.calls) |c| {
|
||||
var eb: [10]u8 = undefined;
|
||||
try w.print("type::call,expiration::{s},strike:num:{d}", .{ chain.expiration.format(&eb), c.strike });
|
||||
if (c.bid) |v| try w.print(",bid:num:{d}", .{v});
|
||||
if (c.ask) |v| try w.print(",ask:num:{d}", .{v});
|
||||
if (c.last_price) |v| try w.print(",last:num:{d}", .{v});
|
||||
if (c.volume) |v| try w.print(",volume:num:{d}", .{v});
|
||||
if (c.open_interest) |v| try w.print(",oi:num:{d}", .{v});
|
||||
if (c.implied_volatility) |v| try w.print(",iv:num:{d}", .{v});
|
||||
if (c.delta) |v| try w.print(",delta:num:{d}", .{v});
|
||||
if (c.gamma) |v| try w.print(",gamma:num:{d}", .{v});
|
||||
if (c.theta) |v| try w.print(",theta:num:{d}", .{v});
|
||||
if (c.vega) |v| try w.print(",vega:num:{d}", .{v});
|
||||
try w.writeAll("\n");
|
||||
}
|
||||
|
||||
for (chain.puts) |p| {
|
||||
var eb: [10]u8 = undefined;
|
||||
try w.print("type::put,expiration::{s},strike:num:{d}", .{ chain.expiration.format(&eb), p.strike });
|
||||
if (p.bid) |v| try w.print(",bid:num:{d}", .{v});
|
||||
if (p.ask) |v| try w.print(",ask:num:{d}", .{v});
|
||||
if (p.last_price) |v| try w.print(",last:num:{d}", .{v});
|
||||
if (p.volume) |v| try w.print(",volume:num:{d}", .{v});
|
||||
if (p.open_interest) |v| try w.print(",oi:num:{d}", .{v});
|
||||
if (p.implied_volatility) |v| try w.print(",iv:num:{d}", .{v});
|
||||
if (p.delta) |v| try w.print(",delta:num:{d}", .{v});
|
||||
if (p.gamma) |v| try w.print(",gamma:num:{d}", .{v});
|
||||
if (p.theta) |v| try w.print(",theta:num:{d}", .{v});
|
||||
if (p.vega) |v| try w.print(",vega:num:{d}", .{v});
|
||||
try w.writeAll("\n");
|
||||
}
|
||||
}
|
||||
|
||||
const writer = buf.writer(allocator);
|
||||
try writer.print("{f}", .{srf.fmtFrom(OptionsRecord, allocator, records.items, options)});
|
||||
return buf.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
|
|
@ -720,55 +532,26 @@ pub const Store = struct {
|
|||
chains.deinit(allocator);
|
||||
}
|
||||
|
||||
// First pass: collect chain headers (expirations)
|
||||
// Second: collect calls/puts per expiration
|
||||
var exp_map = std.StringHashMap(usize).init(allocator);
|
||||
// Map expiration date → chain index
|
||||
var exp_map = std.AutoHashMap(i32, usize).init(allocator);
|
||||
defer exp_map.deinit();
|
||||
|
||||
// Collect all chain records first
|
||||
// First pass: collect chain headers
|
||||
for (parsed.records.items) |record| {
|
||||
var rec_type: []const u8 = "";
|
||||
var expiration: ?Date = null;
|
||||
var exp_str: []const u8 = "";
|
||||
var symbol: []const u8 = "";
|
||||
var price: ?f64 = null;
|
||||
|
||||
for (record.fields) |field| {
|
||||
if (std.mem.eql(u8, field.key, "type")) {
|
||||
if (field.value) |v| rec_type = switch (v) {
|
||||
.string => |s| s,
|
||||
else => "",
|
||||
};
|
||||
} else if (std.mem.eql(u8, field.key, "expiration")) {
|
||||
if (field.value) |v| {
|
||||
exp_str = switch (v) {
|
||||
.string => |s| s,
|
||||
else => continue,
|
||||
};
|
||||
expiration = Date.parse(exp_str) catch null;
|
||||
}
|
||||
} else if (std.mem.eql(u8, field.key, "symbol")) {
|
||||
if (field.value) |v| symbol = switch (v) {
|
||||
.string => |s| s,
|
||||
else => "",
|
||||
};
|
||||
} else if (std.mem.eql(u8, field.key, "price")) {
|
||||
if (field.value) |v| price = numVal(v);
|
||||
}
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, rec_type, "chain")) {
|
||||
if (expiration) |exp| {
|
||||
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, symbol),
|
||||
.underlying_price = price,
|
||||
.expiration = exp,
|
||||
.underlying_symbol = try allocator.dupe(u8, ch.symbol),
|
||||
.underlying_price = ch.price,
|
||||
.expiration = ch.expiration,
|
||||
.calls = &.{},
|
||||
.puts = &.{},
|
||||
});
|
||||
try exp_map.put(exp_str, idx);
|
||||
}
|
||||
try exp_map.put(ch.expiration.days, idx);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -787,67 +570,23 @@ pub const Store = struct {
|
|||
}
|
||||
|
||||
for (parsed.records.items) |record| {
|
||||
var rec_type: []const u8 = "";
|
||||
var exp_str: []const u8 = "";
|
||||
var contract = OptionContract{
|
||||
.contract_type = .call,
|
||||
.strike = 0,
|
||||
.expiration = Date.epoch,
|
||||
};
|
||||
|
||||
for (record.fields) |field| {
|
||||
if (std.mem.eql(u8, field.key, "type")) {
|
||||
if (field.value) |v| rec_type = switch (v) {
|
||||
.string => |s| s,
|
||||
else => "",
|
||||
};
|
||||
} else if (std.mem.eql(u8, field.key, "expiration")) {
|
||||
if (field.value) |v| {
|
||||
exp_str = switch (v) {
|
||||
.string => |s| s,
|
||||
else => continue,
|
||||
};
|
||||
contract.expiration = Date.parse(exp_str) catch Date.epoch;
|
||||
}
|
||||
} else if (std.mem.eql(u8, field.key, "strike")) {
|
||||
if (field.value) |v| contract.strike = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "bid")) {
|
||||
if (field.value) |v| contract.bid = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "ask")) {
|
||||
if (field.value) |v| contract.ask = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "last")) {
|
||||
if (field.value) |v| contract.last_price = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "volume")) {
|
||||
if (field.value) |v| contract.volume = @intFromFloat(numVal(v));
|
||||
} else if (std.mem.eql(u8, field.key, "oi")) {
|
||||
if (field.value) |v| contract.open_interest = @intFromFloat(numVal(v));
|
||||
} else if (std.mem.eql(u8, field.key, "iv")) {
|
||||
if (field.value) |v| contract.implied_volatility = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "delta")) {
|
||||
if (field.value) |v| contract.delta = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "gamma")) {
|
||||
if (field.value) |v| contract.gamma = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "theta")) {
|
||||
if (field.value) |v| contract.theta = numVal(v);
|
||||
} else if (std.mem.eql(u8, field.key, "vega")) {
|
||||
if (field.value) |v| contract.vega = numVal(v);
|
||||
}
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, rec_type, "call")) {
|
||||
contract.contract_type = .call;
|
||||
if (exp_map.get(exp_str)) |idx| {
|
||||
const opt_rec = record.to(OptionsRecord) catch continue;
|
||||
switch (opt_rec) {
|
||||
.call => |cf| {
|
||||
if (exp_map.get(cf.expiration.days)) |idx| {
|
||||
const entry = try calls_map.getOrPut(idx);
|
||||
if (!entry.found_existing) entry.value_ptr.* = .empty;
|
||||
try entry.value_ptr.append(allocator, contract);
|
||||
try entry.value_ptr.append(allocator, fieldsToContract(cf, .call));
|
||||
}
|
||||
} else if (std.mem.eql(u8, rec_type, "put")) {
|
||||
contract.contract_type = .put;
|
||||
if (exp_map.get(exp_str)) |idx| {
|
||||
},
|
||||
.put => |cf| {
|
||||
if (exp_map.get(cf.expiration.days)) |idx| {
|
||||
const entry = try puts_map.getOrPut(idx);
|
||||
if (!entry.found_existing) entry.value_ptr.* = .empty;
|
||||
try entry.value_ptr.append(allocator, contract);
|
||||
try entry.value_ptr.append(allocator, fieldsToContract(cf, .put));
|
||||
}
|
||||
},
|
||||
.chain => {},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -864,21 +603,6 @@ pub const Store = struct {
|
|||
return chains.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn parseDividendTypeTag(s: []const u8) DividendType {
|
||||
if (std.mem.eql(u8, s, "regular")) return .regular;
|
||||
if (std.mem.eql(u8, s, "special")) return .special;
|
||||
if (std.mem.eql(u8, s, "supplemental")) return .supplemental;
|
||||
if (std.mem.eql(u8, s, "irregular")) return .irregular;
|
||||
return .unknown;
|
||||
}
|
||||
|
||||
fn parseReportTimeTag(s: []const u8) ReportTime {
|
||||
if (std.mem.eql(u8, s, "bmo")) return .bmo;
|
||||
if (std.mem.eql(u8, s, "amc")) return .amc;
|
||||
if (std.mem.eql(u8, s, "dmh")) return .dmh;
|
||||
return .unknown;
|
||||
}
|
||||
|
||||
fn symbolPath(self: *Store, symbol: []const u8, file_name: []const u8) ![]const u8 {
|
||||
if (file_name.len == 0) {
|
||||
return std.fs.path.join(self.allocator, &.{ self.cache_dir, symbol });
|
||||
|
|
@ -1066,7 +790,7 @@ test "dividend serialize/deserialize round-trip" {
|
|||
.{ .ex_date = Date.fromYmd(2024, 6, 14), .amount = 0.9148, .type = .special },
|
||||
};
|
||||
|
||||
const data = try Store.serializeDividends(allocator, &divs);
|
||||
const data = try Store.serializeDividends(allocator, &divs, .{});
|
||||
defer allocator.free(data);
|
||||
|
||||
const parsed = try Store.deserializeDividends(allocator, data);
|
||||
|
|
@ -1094,7 +818,7 @@ test "split serialize/deserialize round-trip" {
|
|||
.{ .date = Date.fromYmd(2014, 6, 9), .numerator = 7, .denominator = 1 },
|
||||
};
|
||||
|
||||
const data = try Store.serializeSplits(allocator, &splits);
|
||||
const data = try Store.serializeSplits(allocator, &splits, .{});
|
||||
defer allocator.free(data);
|
||||
|
||||
const parsed = try Store.deserializeSplits(allocator, data);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const
|
|||
allocator.free(h);
|
||||
}
|
||||
if (profile.sectors) |s| {
|
||||
for (s) |sec| allocator.free(sec.sector);
|
||||
for (s) |sec| allocator.free(sec.name);
|
||||
allocator.free(s);
|
||||
}
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ pub fn printProfile(profile: zfin.EtfProfile, symbol: []const u8, color: bool, o
|
|||
try cli.setFg(out, color, cli.CLR_ACCENT);
|
||||
try out.print(" {d:>5.1}%", .{sec.weight * 100.0});
|
||||
try cli.reset(out, color);
|
||||
try out.print(" {s}\n", .{sec.sector});
|
||||
try out.print(" {s}\n", .{sec.name});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ pub const Holding = struct {
|
|||
|
||||
/// Sector allocation in an ETF.
|
||||
pub const SectorWeight = struct {
|
||||
sector: []const u8,
|
||||
name: []const u8,
|
||||
weight: f64,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ fn parseEtfProfileResponse(
|
|||
|
||||
const duped_name = allocator.dupe(u8, name) catch return provider.ProviderError.OutOfMemory;
|
||||
sectors.append(allocator, .{
|
||||
.sector = duped_name,
|
||||
.name = duped_name,
|
||||
.weight = weight,
|
||||
}) catch return provider.ProviderError.OutOfMemory;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,8 +132,7 @@ pub const DataService = struct {
|
|||
const cached_raw = s.readRaw(symbol, .candles_daily) catch return DataError.CacheError;
|
||||
if (cached_raw) |data| {
|
||||
defer self.allocator.free(data);
|
||||
const fresh = s.isFresh(symbol, .candles_daily) catch false;
|
||||
if (fresh) {
|
||||
if (cache.Store.isFreshData(data, self.allocator)) {
|
||||
const candles = cache.Store.deserializeCandles(self.allocator, data) catch null;
|
||||
if (candles) |c| return .{ .data = c, .source = .cached, .timestamp = s.getMtime(symbol, .candles_daily) orelse std.time.timestamp() };
|
||||
}
|
||||
|
|
@ -150,9 +149,10 @@ pub const DataService = struct {
|
|||
|
||||
// Cache the result
|
||||
if (fetched.len > 0) {
|
||||
if (cache.Store.serializeCandles(self.allocator, fetched)) |srf_data| {
|
||||
const expires = std.time.timestamp() + cache.Ttl.candles_latest;
|
||||
if (cache.Store.serializeCandles(self.allocator, fetched, .{ .expires = expires })) |srf_data| {
|
||||
defer self.allocator.free(srf_data);
|
||||
s.writeWithExpiry(symbol, .candles_daily, srf_data, cache.Ttl.candles_latest) catch {};
|
||||
s.writeRaw(symbol, .candles_daily, srf_data) catch {};
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
|
|
@ -167,8 +167,7 @@ pub const DataService = struct {
|
|||
const cached_raw = s.readRaw(symbol, .dividends) catch return DataError.CacheError;
|
||||
if (cached_raw) |data| {
|
||||
defer self.allocator.free(data);
|
||||
const fresh = s.isFresh(symbol, .dividends) catch false;
|
||||
if (fresh) {
|
||||
if (cache.Store.isFreshData(data, self.allocator)) {
|
||||
const divs = cache.Store.deserializeDividends(self.allocator, data) catch null;
|
||||
if (divs) |d| return .{ .data = d, .source = .cached, .timestamp = s.getMtime(symbol, .dividends) orelse std.time.timestamp() };
|
||||
}
|
||||
|
|
@ -180,9 +179,10 @@ pub const DataService = struct {
|
|||
};
|
||||
|
||||
if (fetched.len > 0) {
|
||||
if (cache.Store.serializeDividends(self.allocator, fetched)) |srf_data| {
|
||||
const expires = std.time.timestamp() + cache.Ttl.dividends;
|
||||
if (cache.Store.serializeDividends(self.allocator, fetched, .{ .expires = expires })) |srf_data| {
|
||||
defer self.allocator.free(srf_data);
|
||||
s.writeWithExpiry(symbol, .dividends, srf_data, cache.Ttl.dividends) catch {};
|
||||
s.writeRaw(symbol, .dividends, srf_data) catch {};
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
|
|
@ -197,8 +197,7 @@ pub const DataService = struct {
|
|||
const cached_raw = s.readRaw(symbol, .splits) catch return DataError.CacheError;
|
||||
if (cached_raw) |data| {
|
||||
defer self.allocator.free(data);
|
||||
const fresh = s.isFresh(symbol, .splits) catch false;
|
||||
if (fresh) {
|
||||
if (cache.Store.isFreshData(data, self.allocator)) {
|
||||
const splits = cache.Store.deserializeSplits(self.allocator, data) catch null;
|
||||
if (splits) |sp| return .{ .data = sp, .source = .cached, .timestamp = s.getMtime(symbol, .splits) orelse std.time.timestamp() };
|
||||
}
|
||||
|
|
@ -209,9 +208,9 @@ pub const DataService = struct {
|
|||
return DataError.FetchFailed;
|
||||
};
|
||||
|
||||
if (cache.Store.serializeSplits(self.allocator, fetched)) |srf_data| {
|
||||
if (cache.Store.serializeSplits(self.allocator, fetched, .{ .expires = std.time.timestamp() + cache.Ttl.splits })) |srf_data| {
|
||||
defer self.allocator.free(srf_data);
|
||||
s.writeWithExpiry(symbol, .splits, srf_data, cache.Ttl.splits) catch {};
|
||||
s.writeRaw(symbol, .splits, srf_data) catch {};
|
||||
} else |_| {}
|
||||
|
||||
return .{ .data = fetched, .source = .fetched, .timestamp = std.time.timestamp() };
|
||||
|
|
@ -225,8 +224,7 @@ pub const DataService = struct {
|
|||
const cached_raw = s.readRaw(symbol, .options) catch return DataError.CacheError;
|
||||
if (cached_raw) |data| {
|
||||
defer self.allocator.free(data);
|
||||
const fresh = s.isFresh(symbol, .options) catch false;
|
||||
if (fresh) {
|
||||
if (cache.Store.isFreshData(data, self.allocator)) {
|
||||
const chains = cache.Store.deserializeOptions(self.allocator, data) catch null;
|
||||
if (chains) |c| return .{ .data = c, .source = .cached, .timestamp = s.getMtime(symbol, .options) orelse std.time.timestamp() };
|
||||
}
|
||||
|
|
@ -238,9 +236,10 @@ pub const DataService = struct {
|
|||
};
|
||||
|
||||
if (fetched.len > 0) {
|
||||
if (cache.Store.serializeOptions(self.allocator, fetched)) |srf_data| {
|
||||
const expires = std.time.timestamp() + cache.Ttl.options;
|
||||
if (cache.Store.serializeOptions(self.allocator, fetched, .{ .expires = expires })) |srf_data| {
|
||||
defer self.allocator.free(srf_data);
|
||||
s.writeWithExpiry(symbol, .options, srf_data, cache.Ttl.options) catch {};
|
||||
s.writeRaw(symbol, .options, srf_data) catch {};
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
|
|
@ -255,8 +254,7 @@ pub const DataService = struct {
|
|||
const cached_raw = s.readRaw(symbol, .earnings) catch return DataError.CacheError;
|
||||
if (cached_raw) |data| {
|
||||
defer self.allocator.free(data);
|
||||
const fresh = s.isFresh(symbol, .earnings) catch false;
|
||||
if (fresh) {
|
||||
if (cache.Store.isFreshData(data, self.allocator)) {
|
||||
const events = cache.Store.deserializeEarnings(self.allocator, data) catch null;
|
||||
if (events) |e| return .{ .data = e, .source = .cached, .timestamp = s.getMtime(symbol, .earnings) orelse std.time.timestamp() };
|
||||
}
|
||||
|
|
@ -272,9 +270,10 @@ pub const DataService = struct {
|
|||
};
|
||||
|
||||
if (fetched.len > 0) {
|
||||
if (cache.Store.serializeEarnings(self.allocator, fetched)) |srf_data| {
|
||||
const expires = std.time.timestamp() + cache.Ttl.earnings;
|
||||
if (cache.Store.serializeEarnings(self.allocator, fetched, .{ .expires = expires })) |srf_data| {
|
||||
defer self.allocator.free(srf_data);
|
||||
s.writeWithExpiry(symbol, .earnings, srf_data, cache.Ttl.earnings) catch {};
|
||||
s.writeRaw(symbol, .earnings, srf_data) catch {};
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
|
|
@ -289,8 +288,7 @@ pub const DataService = struct {
|
|||
const cached_raw = s.readRaw(symbol, .etf_profile) catch return DataError.CacheError;
|
||||
if (cached_raw) |data| {
|
||||
defer self.allocator.free(data);
|
||||
const fresh = s.isFresh(symbol, .etf_profile) catch false;
|
||||
if (fresh) {
|
||||
if (cache.Store.isFreshData(data, self.allocator)) {
|
||||
const profile = cache.Store.deserializeEtfProfile(self.allocator, data) catch null;
|
||||
if (profile) |p| return .{ .data = p, .source = .cached, .timestamp = s.getMtime(symbol, .etf_profile) orelse std.time.timestamp() };
|
||||
}
|
||||
|
|
@ -301,9 +299,9 @@ pub const DataService = struct {
|
|||
return DataError.FetchFailed;
|
||||
};
|
||||
|
||||
if (cache.Store.serializeEtfProfile(self.allocator, fetched)) |srf_data| {
|
||||
if (cache.Store.serializeEtfProfile(self.allocator, fetched, .{ .expires = std.time.timestamp() + cache.Ttl.etf_profile })) |srf_data| {
|
||||
defer self.allocator.free(srf_data);
|
||||
s.writeWithExpiry(symbol, .etf_profile, srf_data, cache.Ttl.etf_profile) catch {};
|
||||
s.writeRaw(symbol, .etf_profile, srf_data) catch {};
|
||||
} else |_| {}
|
||||
|
||||
return .{ .data = fetched, .source = .fetched, .timestamp = std.time.timestamp() };
|
||||
|
|
@ -396,10 +394,15 @@ pub const DataService = struct {
|
|||
};
|
||||
}
|
||||
|
||||
/// Check if candle data is fresh in cache without reading/deserializing.
|
||||
/// Check if candle data is fresh in cache without full deserialization.
|
||||
pub fn isCandleCacheFresh(self: *DataService, symbol: []const u8) bool {
|
||||
var s = self.store();
|
||||
return s.isFresh(symbol, .candles_daily) catch false;
|
||||
const data = s.readRaw(symbol, .candles_daily) catch return false;
|
||||
if (data) |d| {
|
||||
defer self.allocator.free(d);
|
||||
return cache.Store.isFreshData(d, self.allocator);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Read only the latest close price from cached candles (no full deserialization).
|
||||
|
|
|
|||
|
|
@ -1637,7 +1637,7 @@ const App = struct {
|
|||
self.allocator.free(h);
|
||||
}
|
||||
if (profile.sectors) |s| {
|
||||
for (s) |sec| self.allocator.free(sec.sector);
|
||||
for (s) |sec| self.allocator.free(sec.name);
|
||||
self.allocator.free(s);
|
||||
}
|
||||
}
|
||||
|
|
@ -2924,7 +2924,7 @@ const App = struct {
|
|||
const show = @min(sectors.len, 7);
|
||||
for (sectors[0..show]) |sec| {
|
||||
// Truncate long sector names
|
||||
const name = if (sec.sector.len > 20) sec.sector[0..20] else sec.sector;
|
||||
const name = if (sec.name.len > 20) sec.name[0..20] else sec.name;
|
||||
try col3.add(arena, try std.fmt.allocPrint(arena, " {d:>5.1}% {s}", .{ sec.weight * 100.0, name }), th.contentStyle());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue