migrate enrich to new cli framework
This commit is contained in:
parent
8b9537dbf5
commit
63f5cc445b
2 changed files with 98 additions and 16 deletions
|
|
@ -1,9 +1,63 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
const framework = @import("framework.zig");
|
||||
const fmt = @import("../format.zig");
|
||||
const isCusipLike = @import("../models/portfolio.zig").isCusipLike;
|
||||
|
||||
pub const ParsedArgs = struct {
|
||||
/// Either a symbol (e.g. "AAPL") or a path to a portfolio file
|
||||
/// (e.g. "portfolio.srf"). Distinguished by suffix / path-separator
|
||||
/// heuristic at run time so the user can pass either form.
|
||||
arg: []const u8,
|
||||
};
|
||||
|
||||
pub const meta = struct {
|
||||
pub const name: []const u8 = "enrich";
|
||||
pub const group: framework.Group = .hygiene;
|
||||
pub const synopsis: []const u8 = "Bootstrap metadata.srf from Alpha Vantage (25 req/day limit)";
|
||||
pub const help: []const u8 =
|
||||
\\Usage: zfin enrich <FILE|SYMBOL>
|
||||
\\
|
||||
\\Bootstrap a `metadata.srf` classification file from Alpha
|
||||
\\Vantage's OVERVIEW endpoint. Two modes:
|
||||
\\
|
||||
\\ - File mode (path or `*.srf` suffix): enrich every stock
|
||||
\\ symbol in the portfolio. Output is a complete SRF file
|
||||
\\ written to stdout — redirect into metadata.srf and
|
||||
\\ edit by hand for accuracy.
|
||||
\\ - Symbol mode (anything else): enrich a single symbol and
|
||||
\\ emit one appendable SRF line. Useful for adding to an
|
||||
\\ existing metadata.srf without rerunning the whole file.
|
||||
\\
|
||||
\\Caveats: Alpha Vantage's free tier is 25 requests/day. The
|
||||
\\OVERVIEW data is US-domicile-biased — international ETFs
|
||||
\\classify as `geo::US`. Always review the output before
|
||||
\\saving as `metadata.srf`. Requires ALPHAVANTAGE_API_KEY.
|
||||
\\
|
||||
\\Examples:
|
||||
\\ zfin enrich portfolio.srf > metadata.srf # whole portfolio
|
||||
\\ zfin enrich AAPL >> metadata.srf # single symbol append
|
||||
\\
|
||||
;
|
||||
};
|
||||
|
||||
comptime {
|
||||
framework.validateCommandModule(@This());
|
||||
}
|
||||
|
||||
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
|
||||
if (cmd_args.len < 1) {
|
||||
try cli.stderrPrint(ctx.io, "Error: 'enrich' requires a portfolio file path or symbol\n");
|
||||
return error.MissingArg;
|
||||
}
|
||||
if (cmd_args.len > 1) {
|
||||
try cli.stderrPrint(ctx.io, "Error: 'enrich' takes a single argument (file path or symbol)\n");
|
||||
return error.UnexpectedArg;
|
||||
}
|
||||
return .{ .arg = cmd_args[0] };
|
||||
}
|
||||
|
||||
const OverviewMeta = struct {
|
||||
sector: []const u8,
|
||||
geo: []const u8,
|
||||
|
|
@ -38,20 +92,21 @@ fn deriveMetadata(overview: zfin.CompanyOverview, sector_buf: []u8) OverviewMeta
|
|||
/// Reads the portfolio, extracts stock symbols, fetches sector/industry/country for each,
|
||||
/// and outputs a metadata SRF file to stdout.
|
||||
/// If the argument looks like a symbol (no path separators, no .srf extension), enrich just that symbol.
|
||||
pub fn run(io: std.Io, allocator: std.mem.Allocator, svc: *zfin.DataService, arg: []const u8, as_of: zfin.Date, out: *std.Io.Writer) !void {
|
||||
pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
||||
const svc = ctx.svc orelse return error.MissingDataService;
|
||||
// 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;
|
||||
const is_file = std.mem.endsWith(u8, parsed.arg, ".srf") or
|
||||
std.mem.indexOfScalar(u8, parsed.arg, '/') != null or
|
||||
std.mem.indexOfScalar(u8, parsed.arg, '.') != null;
|
||||
|
||||
if (!is_file) {
|
||||
// Single symbol mode: enrich one symbol, output appendable SRF (no header)
|
||||
try enrichSymbol(io, allocator, svc, arg, out);
|
||||
try enrichSymbol(ctx.io, ctx.allocator, svc, parsed.arg, ctx.out);
|
||||
return;
|
||||
}
|
||||
|
||||
// Portfolio file mode: enrich all symbols
|
||||
try enrichPortfolio(io, allocator, svc, arg, as_of, out);
|
||||
try enrichPortfolio(ctx.io, ctx.allocator, svc, parsed.arg, ctx.today, ctx.out);
|
||||
}
|
||||
|
||||
/// Enrich a single symbol and output appendable SRF lines to stdout.
|
||||
|
|
@ -82,13 +137,13 @@ fn enrichSymbol(io: std.Io, allocator: std.mem.Allocator, svc: *zfin.DataService
|
|||
}
|
||||
|
||||
var sector_buf: [64]u8 = undefined;
|
||||
const meta = deriveMetadata(overview, §or_buf);
|
||||
const derived = deriveMetadata(overview, §or_buf);
|
||||
|
||||
if (overview.name) |name| {
|
||||
try out.print("# {s}\n", .{name});
|
||||
}
|
||||
try out.print("symbol::{s},sector::{s},geo::{s},asset_class::{s}\n", .{
|
||||
sym, meta.sector, meta.geo, meta.asset_class,
|
||||
sym, derived.sector, derived.geo, derived.asset_class,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -174,14 +229,14 @@ fn enrichPortfolio(io: std.Io, allocator: std.mem.Allocator, svc: *zfin.DataServ
|
|||
}
|
||||
|
||||
var sector_buf: [64]u8 = undefined;
|
||||
const meta = deriveMetadata(overview, §or_buf);
|
||||
const derived = deriveMetadata(overview, §or_buf);
|
||||
|
||||
// Comment with the name for readability
|
||||
if (overview.name) |name| {
|
||||
try out.print("# {s}\n", .{name});
|
||||
}
|
||||
try out.print("symbol::{s},sector::{s},geo::{s},asset_class::{s}\n\n", .{
|
||||
sym, meta.sector, meta.geo, meta.asset_class,
|
||||
sym, derived.sector, derived.geo, derived.asset_class,
|
||||
});
|
||||
success += 1;
|
||||
}
|
||||
|
|
@ -193,3 +248,35 @@ fn enrichPortfolio(io: std.Io, allocator: std.mem.Allocator, svc: *zfin.DataServ
|
|||
});
|
||||
try out.print("# Review and edit this file, then save as metadata.srf\n", .{});
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────
|
||||
|
||||
test "parseArgs: accepts a symbol argument" {
|
||||
var ctx: framework.RunCtx = undefined;
|
||||
ctx.io = std.testing.io;
|
||||
const args = [_][]const u8{"AAPL"};
|
||||
const parsed = try parseArgs(&ctx, &args);
|
||||
try std.testing.expectEqualStrings("AAPL", parsed.arg);
|
||||
}
|
||||
|
||||
test "parseArgs: accepts a file path argument" {
|
||||
var ctx: framework.RunCtx = undefined;
|
||||
ctx.io = std.testing.io;
|
||||
const args = [_][]const u8{"portfolio.srf"};
|
||||
const parsed = try parseArgs(&ctx, &args);
|
||||
try std.testing.expectEqualStrings("portfolio.srf", parsed.arg);
|
||||
}
|
||||
|
||||
test "parseArgs: missing arg errors" {
|
||||
var ctx: framework.RunCtx = undefined;
|
||||
ctx.io = std.testing.io;
|
||||
const args = [_][]const u8{};
|
||||
try std.testing.expectError(error.MissingArg, parseArgs(&ctx, &args));
|
||||
}
|
||||
|
||||
test "parseArgs: extra args error" {
|
||||
var ctx: framework.RunCtx = undefined;
|
||||
ctx.io = std.testing.io;
|
||||
const args = [_][]const u8{ "AAPL", "extra" };
|
||||
try std.testing.expectError(error.UnexpectedArg, parseArgs(&ctx, &args));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const command_modules = .{
|
|||
.etf = @import("commands/etf.zig"),
|
||||
|
||||
// Data hygiene
|
||||
.enrich = @import("commands/enrich.zig"),
|
||||
.lookup = @import("commands/lookup.zig"),
|
||||
|
||||
// Infrastructure
|
||||
|
|
@ -508,12 +509,6 @@ fn runCli(init: std.process.Init) !u8 {
|
|||
defer if (wl.resolved) |r| r.deinit(allocator);
|
||||
const wl_path: ?[]const u8 = if (globals.watchlist_path != null or wl.resolved != null) wl.path else null;
|
||||
try commands.portfolio.run(io, allocator, &svc, pf.path, wl_path, force_refresh, today, color, out);
|
||||
} else if (std.mem.eql(u8, command, "enrich")) {
|
||||
if (cmd_args.len < 1) {
|
||||
try cli.stderrPrint(io, "Error: 'enrich' requires a portfolio file path or symbol\n");
|
||||
return 1;
|
||||
}
|
||||
try commands.enrich.run(io, allocator, &svc, cmd_args[0], today, out);
|
||||
} else if (std.mem.eql(u8, command, "audit")) {
|
||||
const pf = resolveUserPath(io, allocator, config, globals.portfolio_path, zfin.Config.default_portfolio_filename);
|
||||
defer if (pf.resolved) |r| r.deinit(allocator);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue