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: Use the `enrich` command to generate a starting `metadata.srf` from Alpha Vantage company overview data:
```bash ```bash
# Enrich an entire portfolio (generates full metadata.srf)
zfin enrich portfolio.srf > 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) ## Account metadata (accounts.srf)
@ -455,7 +459,7 @@ Commands:
etf <SYMBOL> ETF profile (expense ratio, holdings, sectors) etf <SYMBOL> ETF profile (expense ratio, holdings, sectors)
portfolio [FILE] Portfolio summary (default: portfolio.srf) portfolio [FILE] Portfolio summary (default: portfolio.srf)
analysis [FILE] Portfolio analysis breakdowns (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 lookup <CUSIP> CUSIP to ticker lookup via OpenFIGI
cache stats Show cached symbols cache stats Show cached symbols
cache clear Delete all cached data cache clear Delete all cached data

View file

@ -18,6 +18,7 @@ const usage =
\\ etf <SYMBOL> Show ETF profile (holdings, sectors, expense ratio) \\ etf <SYMBOL> Show ETF profile (holdings, sectors, expense ratio)
\\ portfolio [FILE] Load and analyze a portfolio (default: portfolio.srf) \\ portfolio [FILE] Load and analyze a portfolio (default: portfolio.srf)
\\ analysis [FILE] Show portfolio analysis (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 \\ lookup <CUSIP> Look up CUSIP to ticker via OpenFIGI
\\ cache stats Show cache statistics \\ cache stats Show cache statistics
\\ cache clear Clear all cached data \\ 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"); if (args.len < 3) return try stderr_print("Error: 'cache' requires a subcommand (stats, clear)\n");
try cmdCache(allocator, config, args[2]); try cmdCache(allocator, config, args[2]);
} else if (std.mem.eql(u8, command, "enrich")) { } 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]); try cmdEnrich(allocator, config, args[2]);
} else if (std.mem.eql(u8, command, "analysis")) { } else if (std.mem.eql(u8, command, "analysis")) {
// File path is first non-flag arg (default: portfolio.srf) // 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. /// 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, /// Reads the portfolio, extracts stock symbols, fetches sector/industry/country for each,
/// and outputs a metadata SRF file to stdout. /// 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; 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 // Load portfolio
const file_data = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch { 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); const syms = try portfolio.stockSymbols(allocator);
defer allocator.free(syms); 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); var av = AV.init(allocator, av_key);
defer av.deinit(); defer av.deinit();