zfin/src/models/etf_profile.zig

94 lines
3.2 KiB
Zig

const Date = @import("date.zig").Date;
/// Top holding in an ETF.
pub const Holding = struct {
symbol: ?[]const u8 = null,
name: []const u8,
weight: f64,
};
/// Sector allocation in an ETF.
pub const SectorWeight = struct {
name: []const u8,
weight: f64,
};
/// ETF profile and metadata.
pub const EtfProfile = struct {
symbol: []const u8,
name: ?[]const u8 = null,
asset_class: ?[]const u8 = null,
/// Expense ratio as a decimal (e.g., 0.0003 for 0.03%)
expense_ratio: ?f64 = null,
/// Net assets in USD
net_assets: ?f64 = null,
/// Morningstar-style category (e.g., "Large Blend")
category: ?[]const u8 = null,
/// Investment focus description
description: ?[]const u8 = null,
/// Top holdings
holdings: ?[]const Holding = null,
/// Number of total holdings in the fund
total_holdings: ?u32 = null,
/// Sector allocations
sectors: ?[]const SectorWeight = null,
/// Dividend yield as decimal (e.g., 0.0111 for 1.11%)
dividend_yield: ?f64 = null,
/// Portfolio turnover as decimal
portfolio_turnover: ?f64 = null,
/// Fund inception date
inception_date: ?Date = null,
/// Whether the fund is leveraged
leveraged: bool = false,
/// Returns true if the profile contains meaningful ETF data.
/// Non-ETF symbols return empty profiles from Alpha Vantage.
pub fn isEtf(self: EtfProfile) bool {
return self.expense_ratio != null or
self.net_assets != null or
self.holdings != null or
self.sectors != null or
self.total_holdings != null;
}
/// Free any owned fields on this profile.
///
/// Matches the inline cleanup previously inlined in
/// `src/commands/etf.zig`. Only `holdings` and `sectors` are
/// freed here — the top-level optional strings (`name`,
/// `asset_class`, `category`, `description`) are borrowed from
/// the cache store's shared buffer in the provider-fetched path
/// and don't need freeing. If that changes (e.g., a provider
/// starts allocating each field separately), extend this
/// function accordingly.
pub fn deinit(self: EtfProfile, allocator: std.mem.Allocator) void {
if (self.holdings) |h| {
for (h) |holding| {
if (holding.symbol) |s| allocator.free(s);
allocator.free(holding.name);
}
allocator.free(h);
}
if (self.sectors) |s| {
for (s) |sec| allocator.free(sec.name);
allocator.free(s);
}
}
};
const std = @import("std");
test "isEtf" {
// Empty profile -> not an ETF
const empty = EtfProfile{ .symbol = "AAPL" };
try std.testing.expect(!empty.isEtf());
// Has expense_ratio -> is ETF
const with_er = EtfProfile{ .symbol = "VTI", .expense_ratio = 0.0003 };
try std.testing.expect(with_er.isEtf());
// Has net_assets -> is ETF
const with_na = EtfProfile{ .symbol = "SPY", .net_assets = 500_000_000_000 };
try std.testing.expect(with_na.isEtf());
// Has total_holdings -> is ETF
const with_th = EtfProfile{ .symbol = "QQQ", .total_holdings = 100 };
try std.testing.expect(with_th.isEtf());
}