ai: enhance enrich command for incremental updates

This commit is contained in:
Emil Lerch 2026-02-27 08:21:41 -08:00
parent 6e78818f1c
commit 370cccbfcb
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 93 additions and 9 deletions

View file

@ -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 <SYMBOL> ETF profile (expense ratio, holdings, sectors)
portfolio [FILE] Portfolio summary (default: portfolio.srf)
analysis [FILE] Portfolio analysis breakdowns (default: portfolio.srf)
enrich <FILE> Generate metadata.srf from Alpha Vantage
enrich <FILE|SYMBOL> Generate metadata.srf from Alpha Vantage
lookup <CUSIP> CUSIP to ticker lookup via OpenFIGI
cache stats Show cached symbols
cache clear Delete all cached data

View file

@ -18,6 +18,7 @@ const usage =
\\ etf <SYMBOL> 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 <FILE|SYMBOL> Bootstrap metadata.srf from Alpha Vantage (25 req/day limit)
\\ lookup <CUSIP> 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();