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()); }