104 lines
4.2 KiB
Zig
104 lines
4.2 KiB
Zig
/// Classification metadata for portfolio analysis.
|
|
///
|
|
/// Each entry maps a symbol to one or more asset class / sector / geographic allocations.
|
|
/// For individual stocks, there's typically one entry at 100%.
|
|
/// For blended funds (e.g., target date), there can be multiple entries that sum to ~100%.
|
|
///
|
|
/// Loaded from a metadata SRF file like `metadata.srf`:
|
|
/// symbol::AMZN,sector::Technology,geo::US,asset_class::US Large Cap
|
|
/// symbol::02315N600,asset_class::US Large Cap,pct:num:55
|
|
/// symbol::02315N600,asset_class::International Developed,pct:num:20
|
|
/// symbol::02315N600,asset_class::Bonds,pct:num:15
|
|
const std = @import("std");
|
|
const srf = @import("srf");
|
|
|
|
/// A single classification entry for a symbol.
|
|
pub const ClassificationEntry = struct {
|
|
symbol: []const u8,
|
|
/// Sector (e.g., "Technology", "Healthcare", "Financials")
|
|
sector: ?[]const u8 = null,
|
|
/// Geographic region (e.g., "US", "International Developed", "Emerging Markets")
|
|
geo: ?[]const u8 = null,
|
|
/// Asset class (e.g., "US Large Cap", "Bonds", "Cash")
|
|
asset_class: ?[]const u8 = null,
|
|
/// Percentage weight for this entry (0-100). Default 100 for single-class assets.
|
|
pct: f64 = 100.0,
|
|
};
|
|
|
|
/// Parsed classification data for the entire portfolio.
|
|
pub const ClassificationMap = struct {
|
|
entries: []ClassificationEntry,
|
|
allocator: std.mem.Allocator,
|
|
|
|
pub fn deinit(self: *ClassificationMap) void {
|
|
for (self.entries) |e| {
|
|
self.allocator.free(e.symbol);
|
|
if (e.sector) |s| self.allocator.free(s);
|
|
if (e.geo) |g| self.allocator.free(g);
|
|
if (e.asset_class) |a| self.allocator.free(a);
|
|
}
|
|
self.allocator.free(self.entries);
|
|
}
|
|
};
|
|
|
|
/// Parse a metadata SRF file into a ClassificationMap.
|
|
/// Each record has: symbol::<SYM>,sector::<S>,geo::<G>,asset_class::<A>,pct:num:<P>
|
|
/// All fields except symbol are optional. pct defaults to 100.
|
|
pub fn parseClassificationFile(allocator: std.mem.Allocator, data: []const u8) !ClassificationMap {
|
|
var entries = std.ArrayList(ClassificationEntry).empty;
|
|
errdefer {
|
|
for (entries.items) |e| {
|
|
allocator.free(e.symbol);
|
|
if (e.sector) |s| allocator.free(s);
|
|
if (e.geo) |g| allocator.free(g);
|
|
if (e.asset_class) |a| allocator.free(a);
|
|
}
|
|
entries.deinit(allocator);
|
|
}
|
|
|
|
var reader = std.Io.Reader.fixed(data);
|
|
var it = srf.iterator(&reader, allocator, .{ .alloc_strings = false }) catch return error.InvalidData;
|
|
defer it.deinit();
|
|
|
|
while (try it.next()) |fields| {
|
|
const entry = fields.to(ClassificationEntry) catch continue;
|
|
try entries.append(allocator, .{
|
|
.symbol = try allocator.dupe(u8, entry.symbol),
|
|
.sector = if (entry.sector) |s| try allocator.dupe(u8, s) else null,
|
|
.geo = if (entry.geo) |g| try allocator.dupe(u8, g) else null,
|
|
.asset_class = if (entry.asset_class) |a| try allocator.dupe(u8, a) else null,
|
|
.pct = entry.pct,
|
|
});
|
|
}
|
|
|
|
return .{
|
|
.entries = try entries.toOwnedSlice(allocator),
|
|
.allocator = allocator,
|
|
};
|
|
}
|
|
|
|
test "parse classification file" {
|
|
const data =
|
|
\\#!srfv1
|
|
\\# Stock: single sector
|
|
\\symbol::AMZN,sector::Technology,geo::US,asset_class::US Large Cap
|
|
\\
|
|
\\# Target date fund: blended
|
|
\\symbol::TGT2035,asset_class::US Large Cap,pct:num:55
|
|
\\symbol::TGT2035,asset_class::Bonds,pct:num:15
|
|
\\symbol::TGT2035,asset_class::International Developed,pct:num:20
|
|
;
|
|
const allocator = std.testing.allocator;
|
|
var cm = try parseClassificationFile(allocator, data);
|
|
defer cm.deinit();
|
|
|
|
try std.testing.expectEqual(@as(usize, 4), cm.entries.len);
|
|
try std.testing.expectEqualStrings("AMZN", cm.entries[0].symbol);
|
|
try std.testing.expectEqualStrings("Technology", cm.entries[0].sector.?);
|
|
try std.testing.expectEqualStrings("US", cm.entries[0].geo.?);
|
|
try std.testing.expectApproxEqAbs(@as(f64, 100.0), cm.entries[0].pct, 0.01);
|
|
|
|
try std.testing.expectEqualStrings("TGT2035", cm.entries[1].symbol);
|
|
try std.testing.expectEqualStrings("US Large Cap", cm.entries[1].asset_class.?);
|
|
try std.testing.expectApproxEqAbs(@as(f64, 55.0), cm.entries[1].pct, 0.01);
|
|
}
|