182 lines
6.5 KiB
Zig
182 lines
6.5 KiB
Zig
const std = @import("std");
|
|
const zfin = @import("../root.zig");
|
|
const cli = @import("common.zig");
|
|
const framework = @import("framework.zig");
|
|
const isCusipLike = @import("../models/portfolio.zig").isCusipLike;
|
|
|
|
pub const ParsedArgs = struct {
|
|
cusip: []const u8,
|
|
};
|
|
|
|
pub const meta = struct {
|
|
pub const name: []const u8 = "lookup";
|
|
pub const group: framework.Group = .hygiene;
|
|
pub const synopsis: []const u8 = "Look up a CUSIP to ticker via OpenFIGI";
|
|
pub const uppercase_first_arg: bool = true;
|
|
pub const help: []const u8 =
|
|
\\Usage: zfin lookup <CUSIP>
|
|
\\
|
|
\\Look up a CUSIP (9-char alphanumeric security identifier) to
|
|
\\its ticker via the OpenFIGI API. Successful results are cached
|
|
\\indefinitely in `cusip_tickers.srf`. OPENFIGI_API_KEY raises
|
|
\\the rate limit but the unauthenticated tier works for low
|
|
\\volume.
|
|
\\
|
|
\\Mutual funds frequently have no OpenFIGI coverage; the
|
|
\\command surfaces that and suggests a manual portfolio entry.
|
|
\\
|
|
\\Examples:
|
|
\\ zfin lookup 037833100 # → AAPL
|
|
\\
|
|
;
|
|
};
|
|
|
|
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: 'lookup' requires a CUSIP argument\n");
|
|
return error.MissingCusip;
|
|
}
|
|
if (cmd_args.len > 1) {
|
|
try cli.stderrPrint(ctx.io, "Error: 'lookup' takes a single CUSIP argument\n");
|
|
return error.UnexpectedArg;
|
|
}
|
|
return .{ .cusip = cmd_args[0] };
|
|
}
|
|
|
|
pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
|
const svc = ctx.svc orelse return error.MissingDataService;
|
|
const out = ctx.out;
|
|
const color = ctx.color;
|
|
const allocator = ctx.allocator;
|
|
if (!isCusipLike(parsed.cusip)) {
|
|
try cli.printFg(out, color, cli.CLR_MUTED, "Note: '{s}' doesn't look like a CUSIP (expected 9 alphanumeric chars with digits)\n", .{parsed.cusip});
|
|
}
|
|
|
|
try cli.stderrPrint(ctx.io, "Looking up via OpenFIGI...\n");
|
|
|
|
// Try full batch lookup for richer output
|
|
const results = svc.lookupCusips(&.{parsed.cusip}) catch {
|
|
try cli.stderrPrint(ctx.io, "Error: OpenFIGI request failed (network error)\n");
|
|
return;
|
|
};
|
|
defer {
|
|
for (results) |r| {
|
|
if (r.ticker) |t| allocator.free(t);
|
|
if (r.name) |n| allocator.free(n);
|
|
if (r.security_type) |s| allocator.free(s);
|
|
}
|
|
allocator.free(results);
|
|
}
|
|
|
|
if (results.len == 0 or !results[0].found) {
|
|
try out.print("No result from OpenFIGI for '{s}'\n", .{parsed.cusip});
|
|
return;
|
|
}
|
|
|
|
try display(results[0], parsed.cusip, color, out);
|
|
|
|
// Also cache it
|
|
if (results[0].ticker) |ticker| {
|
|
svc.cacheCusipTicker(parsed.cusip, ticker);
|
|
}
|
|
}
|
|
|
|
pub fn display(result: zfin.CusipResult, cusip: []const u8, color: bool, out: *std.Io.Writer) !void {
|
|
if (result.ticker) |ticker| {
|
|
try cli.printBold(out, color, "{s}", .{cusip});
|
|
try out.print(" -> ", .{});
|
|
try cli.printFg(out, color, cli.CLR_ACCENT, "{s}\n", .{ticker});
|
|
|
|
if (result.name) |name| {
|
|
try cli.printFg(out, color, cli.CLR_MUTED, " Name: {s}\n", .{name});
|
|
}
|
|
if (result.security_type) |st| {
|
|
try cli.printFg(out, color, cli.CLR_MUTED, " Type: {s}\n", .{st});
|
|
}
|
|
|
|
try out.print("\n To use in portfolio: ticker::{s}\n", .{ticker});
|
|
} else {
|
|
try out.print("No ticker found for CUSIP '{s}'\n", .{cusip});
|
|
if (result.name) |name| {
|
|
try cli.printFg(out, color, cli.CLR_MUTED, " Name: {s}\n", .{name});
|
|
}
|
|
try out.print("\n Tip: For mutual funds, OpenFIGI often has no coverage.\n", .{});
|
|
try out.print(" Add manually: symbol::{s},ticker::XXXX,...\n", .{cusip});
|
|
}
|
|
}
|
|
|
|
// ── Tests ────────────────────────────────────────────────────
|
|
|
|
test "parseArgs: accepts a single CUSIP" {
|
|
var ctx: framework.RunCtx = undefined;
|
|
ctx.io = std.testing.io;
|
|
const args = [_][]const u8{"037833100"};
|
|
const parsed = try parseArgs(&ctx, &args);
|
|
try std.testing.expectEqualStrings("037833100", parsed.cusip);
|
|
}
|
|
|
|
test "parseArgs: missing CUSIP errors" {
|
|
var ctx: framework.RunCtx = undefined;
|
|
ctx.io = std.testing.io;
|
|
const args = [_][]const u8{};
|
|
try std.testing.expectError(error.MissingCusip, parseArgs(&ctx, &args));
|
|
}
|
|
|
|
test "parseArgs: extra args error" {
|
|
var ctx: framework.RunCtx = undefined;
|
|
ctx.io = std.testing.io;
|
|
const args = [_][]const u8{ "037833100", "extra" };
|
|
try std.testing.expectError(error.UnexpectedArg, parseArgs(&ctx, &args));
|
|
}
|
|
|
|
test "display shows ticker mapping" {
|
|
var buf: [4096]u8 = undefined;
|
|
var w: std.Io.Writer = .fixed(&buf);
|
|
const result: zfin.CusipResult = .{
|
|
.ticker = "AAPL",
|
|
.name = "Apple Inc",
|
|
.security_type = "Common Stock",
|
|
.found = true,
|
|
};
|
|
try display(result, "037833100", false, &w);
|
|
const out = w.buffered();
|
|
try std.testing.expect(std.mem.indexOf(u8, out, "037833100") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, out, "AAPL") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, out, "Apple Inc") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, out, "Common Stock") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, out, "ticker::AAPL") != null);
|
|
}
|
|
|
|
test "display shows no-ticker message" {
|
|
var buf: [4096]u8 = undefined;
|
|
var w: std.Io.Writer = .fixed(&buf);
|
|
const result: zfin.CusipResult = .{
|
|
.ticker = null,
|
|
.name = "Some Fund",
|
|
.security_type = null,
|
|
.found = true,
|
|
};
|
|
try display(result, "123456789", false, &w);
|
|
const out = w.buffered();
|
|
try std.testing.expect(std.mem.indexOf(u8, out, "No ticker found") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, out, "Some Fund") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, out, "mutual funds") != null);
|
|
}
|
|
|
|
test "display no ANSI without color" {
|
|
var buf: [4096]u8 = undefined;
|
|
var w: std.Io.Writer = .fixed(&buf);
|
|
const result: zfin.CusipResult = .{
|
|
.ticker = "MSFT",
|
|
.name = null,
|
|
.security_type = null,
|
|
.found = true,
|
|
};
|
|
try display(result, "594918104", false, &w);
|
|
const out = w.buffered();
|
|
try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null);
|
|
}
|