diff --git a/README.md b/README.md index f7e589d..2a6336d 100644 --- a/README.md +++ b/README.md @@ -402,10 +402,14 @@ Cash and CD lots are automatically classified as "Cash & CDs" without needing me Use the `enrich` command to generate a starting `metadata.srf` from Alpha Vantage company overview data: ```bash +# Enrich an entire portfolio (generates full metadata.srf) zfin enrich portfolio.srf > metadata.srf + +# Enrich a single symbol and append to existing metadata.srf +zfin enrich SCHD >> metadata.srf ``` -This fetches sector, country, and market cap for each stock symbol and generates classification entries. CUSIPs are skipped (fill in manually). Review and edit the output -- particularly for ETFs and funds where the auto-classification may be too generic. +When given a file path, it fetches all stock symbols and outputs a complete SRF file with headers. When given a symbol, it outputs just the classification lines (no header), so you can append directly with `>>`. ## Account metadata (accounts.srf) @@ -455,7 +459,7 @@ Commands: etf ETF profile (expense ratio, holdings, sectors) portfolio [FILE] Portfolio summary (default: portfolio.srf) analysis [FILE] Portfolio analysis breakdowns (default: portfolio.srf) - enrich Generate metadata.srf from Alpha Vantage + enrich Generate metadata.srf from Alpha Vantage lookup CUSIP to ticker lookup via OpenFIGI cache stats Show cached symbols cache clear Delete all cached data diff --git a/src/cli/main.zig b/src/cli/main.zig index d4fbbfc..a841645 100644 --- a/src/cli/main.zig +++ b/src/cli/main.zig @@ -18,6 +18,7 @@ const usage = \\ etf Show ETF profile (holdings, sectors, expense ratio) \\ portfolio [FILE] Load and analyze a portfolio (default: portfolio.srf) \\ analysis [FILE] Show portfolio analysis (default: portfolio.srf) + \\ enrich Bootstrap metadata.srf from Alpha Vantage (25 req/day limit) \\ lookup Look up CUSIP to ticker via OpenFIGI \\ cache stats Show cache statistics \\ cache clear Clear all cached data @@ -162,7 +163,7 @@ pub fn main() !void { if (args.len < 3) return try stderr_print("Error: 'cache' requires a subcommand (stats, clear)\n"); try cmdCache(allocator, config, args[2]); } else if (std.mem.eql(u8, command, "enrich")) { - if (args.len < 3) return try stderr_print("Error: 'enrich' requires a portfolio file path\n"); + if (args.len < 3) return try stderr_print("Error: 'enrich' requires a portfolio file path or symbol\n"); try cmdEnrich(allocator, config, args[2]); } else if (std.mem.eql(u8, command, "analysis")) { // File path is first non-flag arg (default: portfolio.srf) @@ -2148,8 +2149,92 @@ fn printBreakdownSection(out: anytype, items: []const zfin.analysis.BreakdownIte /// CLI `enrich` command: bootstrap a metadata.srf file from Alpha Vantage OVERVIEW data. /// Reads the portfolio, extracts stock symbols, fetches sector/industry/country for each, /// and outputs a metadata SRF file to stdout. -fn cmdEnrich(allocator: std.mem.Allocator, config: zfin.Config, file_path: []const u8) !void { +/// If the argument looks like a symbol (no path separators, no .srf extension), enrich just that symbol. +fn cmdEnrich(allocator: std.mem.Allocator, config: zfin.Config, arg: []const u8) !void { + // Check for Alpha Vantage API key + const av_key = config.alphavantage_key orelse { + try stderr_print("Error: ALPHAVANTAGE_API_KEY not set. Add it to .env\n"); + return; + }; + + // Determine if arg is a symbol or a file path + const is_file = std.mem.endsWith(u8, arg, ".srf") or + std.mem.indexOfScalar(u8, arg, '/') != null or + std.mem.indexOfScalar(u8, arg, '.') != null; + + if (!is_file) { + // Single symbol mode: enrich one symbol, output appendable SRF (no header) + try cmdEnrichSymbol(allocator, av_key, arg); + return; + } + + // Portfolio file mode: enrich all symbols + try cmdEnrichPortfolio(allocator, config, av_key, arg); +} + +/// Enrich a single symbol and output appendable SRF lines to stdout. +fn cmdEnrichSymbol(allocator: std.mem.Allocator, av_key: []const u8, sym: []const u8) !void { const AV = @import("zfin").AlphaVantage; + var av = AV.init(allocator, av_key); + defer av.deinit(); + + var buf: [32768]u8 = undefined; + var writer = std.fs.File.stdout().writer(&buf); + const out = &writer.interface; + + { + var msg_buf: [128]u8 = undefined; + const msg = std.fmt.bufPrint(&msg_buf, " Fetching {s}...\n", .{sym}) catch " ...\n"; + try stderr_print(msg); + } + + const overview = av.fetchCompanyOverview(allocator, sym) catch { + try stderr_print("Error: Failed to fetch data for symbol\n"); + try out.print("# {s} -- fetch failed\n", .{sym}); + try out.print("# symbol::{s},sector::TODO,geo::TODO,asset_class::TODO\n", .{sym}); + try out.flush(); + return; + }; + defer { + if (overview.name) |n| allocator.free(n); + if (overview.sector) |s| allocator.free(s); + if (overview.industry) |ind| allocator.free(ind); + if (overview.country) |c| allocator.free(c); + if (overview.market_cap) |mc| allocator.free(mc); + if (overview.asset_type) |at| allocator.free(at); + } + + const sector_str = overview.sector orelse "Unknown"; + const country_str = overview.country orelse "US"; + const geo_str = if (std.mem.eql(u8, country_str, "USA")) "US" else country_str; + + const asset_class_str = blk: { + if (overview.asset_type) |at| { + if (std.mem.eql(u8, at, "ETF")) break :blk "ETF"; + if (std.mem.eql(u8, at, "Mutual Fund")) break :blk "Mutual Fund"; + } + if (overview.market_cap) |mc_str| { + const mc = std.fmt.parseInt(u64, mc_str, 10) catch 0; + if (mc >= 10_000_000_000) break :blk "US Large Cap"; + if (mc >= 2_000_000_000) break :blk "US Mid Cap"; + break :blk "US Small Cap"; + } + break :blk "US Large Cap"; + }; + + if (overview.name) |name| { + try out.print("# {s}\n", .{name}); + } + try out.print("symbol::{s},sector::{s},geo::{s},asset_class::{s}\n", .{ + sym, sector_str, geo_str, asset_class_str, + }); + try out.flush(); +} + +/// Enrich all symbols from a portfolio file. +fn cmdEnrichPortfolio(allocator: std.mem.Allocator, config: zfin.Config, av_key: []const u8, file_path: []const u8) !void { + const AV = @import("zfin").AlphaVantage; + _ = config; // Load portfolio const file_data = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch { @@ -2172,11 +2257,6 @@ fn cmdEnrich(allocator: std.mem.Allocator, config: zfin.Config, file_path: []con const syms = try portfolio.stockSymbols(allocator); defer allocator.free(syms); - // Check for Alpha Vantage API key - const av_key = config.alphavantage_key orelse { - try stderr_print("Error: ALPHAVANTAGE_API_KEY not set. Add it to .env\n"); - return; - }; var av = AV.init(allocator, av_key); defer av.deinit();