94 lines
3.2 KiB
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());
|
|
}
|