From 6d9374ab7783273d33e4ddf8d1dc9f3fc156b7b3 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sun, 1 Mar 2026 12:45:26 -0800 Subject: [PATCH] move isCusipLike to a more generic place --- src/analytics/risk.zig | 4 ++-- src/commands/enrich.zig | 2 +- src/commands/lookup.zig | 2 +- src/format.zig | 25 +++++++++++++++++++++++ src/providers/openfigi.zig | 41 ++++++-------------------------------- 5 files changed, 35 insertions(+), 39 deletions(-) diff --git a/src/analytics/risk.zig b/src/analytics/risk.zig index fc9a199..4ec074e 100644 --- a/src/analytics/risk.zig +++ b/src/analytics/risk.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Candle = @import("../models/candle.zig").Candle; const Date = @import("../models/date.zig").Date; -const OpenFigi = @import("../providers/openfigi.zig"); +const fmt = @import("../format.zig"); /// Daily return series statistics. pub const RiskMetrics = struct { @@ -164,7 +164,7 @@ pub fn portfolioSummary( total_realized += pos.realized_pnl; // For CUSIPs with a note, derive a short display label from the note. - const display = if (OpenFigi.isCusipLike(pos.symbol) and pos.note != null) + const display = if (fmt.isCusipLike(pos.symbol) and pos.note != null) shortLabel(pos.note.?) else pos.symbol; diff --git a/src/commands/enrich.zig b/src/commands/enrich.zig index 26a1727..530fa82 100644 --- a/src/commands/enrich.zig +++ b/src/commands/enrich.zig @@ -123,7 +123,7 @@ fn enrichPortfolio(allocator: std.mem.Allocator, av_key: []const u8, file_path: for (syms, 0..) |sym, i| { // Skip CUSIPs and known non-stock symbols - if (zfin.OpenFigi.isCusipLike(sym)) { + if (cli.fmt.isCusipLike(sym)) { // Find the display name for this CUSIP const display: []const u8 = sym; var note: ?[]const u8 = null; diff --git a/src/commands/lookup.zig b/src/commands/lookup.zig index cd53a19..e46041f 100644 --- a/src/commands/lookup.zig +++ b/src/commands/lookup.zig @@ -3,7 +3,7 @@ const zfin = @import("../root.zig"); const cli = @import("common.zig"); pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, cusip: []const u8, color: bool, out: *std.Io.Writer) !void { - if (!zfin.OpenFigi.isCusipLike(cusip)) { + if (!cli.fmt.isCusipLike(cusip)) { try cli.setFg(out, color, cli.CLR_MUTED); try out.print("Note: '{s}' doesn't look like a CUSIP (expected 9 alphanumeric chars with digits)\n", .{cusip}); try cli.reset(out, color); diff --git a/src/format.zig b/src/format.zig index 57ac772..ad0ec0e 100644 --- a/src/format.zig +++ b/src/format.zig @@ -336,6 +336,20 @@ pub fn isMonthlyExpiration(date: Date) bool { return d >= 15 and d <= 21; // 3rd Friday is between 15th and 21st } +/// Check if a string looks like a CUSIP (9 alphanumeric characters). +/// CUSIPs have 6 alphanumeric issuer chars + 2 issue chars + 1 check digit. +/// This is a heuristic — it won't catch all CUSIPs and may have false positives. +pub fn isCusipLike(s: []const u8) bool { + if (s.len != 9) return false; + // Must contain at least one digit (all-alpha would be a ticker) + var has_digit = false; + for (s) |c| { + if (!std.ascii.isAlphanumeric(c)) return false; + if (std.ascii.isDigit(c)) has_digit = true; + } + return has_digit; +} + /// Format an options contract line: strike + last + bid + ask + volume + OI + IV. pub fn fmtContractLine(alloc: std.mem.Allocator, prefix: []const u8, c: OptionContract) ![]const u8 { var last_buf: [12]u8 = undefined; @@ -1045,6 +1059,17 @@ test "isMonthlyExpiration" { try std.testing.expect(!isMonthlyExpiration(Date.fromYmd(2024, 1, 17))); } +test "isCusipLike" { + try std.testing.expect(isCusipLike("02315N600")); // Vanguard Target 2035 + try std.testing.expect(isCusipLike("02315N709")); // Vanguard Target 2040 + try std.testing.expect(isCusipLike("459200101")); // IBM + try std.testing.expect(isCusipLike("06051XJ45")); // CD CUSIP + try std.testing.expect(!isCusipLike("AAPL")); // Too short + try std.testing.expect(!isCusipLike("ABCDEFGHI")); // No digits + try std.testing.expect(isCusipLike("NON40OR52")); // Looks cusip-like (has digits, 9 chars) + try std.testing.expect(!isCusipLike("12345")); // Too short +} + test "lotSortFn" { const open_new = Lot{ .symbol = "A", diff --git a/src/providers/openfigi.zig b/src/providers/openfigi.zig index 2a187b6..3f58977 100644 --- a/src/providers/openfigi.zig +++ b/src/providers/openfigi.zig @@ -109,11 +109,9 @@ pub fn lookupCusips( var argv_list: std.ArrayList([]const u8) = .empty; defer argv_list.deinit(allocator); try argv_list.appendSlice(allocator, &.{ - "curl", "-sS", "--max-time", "30", - "-X", "POST", - "-H", "Content-Type: application/json", - "-H", hdr, - "-d", body_str, + "curl", "-sS", "--max-time", "30", + "-X", "POST", "-H", "Content-Type: application/json", + "-H", hdr, "-d", body_str, api_url, }); @@ -138,11 +136,9 @@ pub fn lookupCusips( var argv_list: std.ArrayList([]const u8) = .empty; defer argv_list.deinit(allocator); try argv_list.appendSlice(allocator, &.{ - "curl", "-sS", "--max-time", "30", - "-X", "POST", - "-H", "Content-Type: application/json", - "-d", body_str, - api_url, + "curl", "-sS", "--max-time", "30", + "-X", "POST", "-H", "Content-Type: application/json", + "-d", body_str, api_url, }); const result = std.process.Child.run(.{ @@ -244,28 +240,3 @@ fn parseResponse(allocator: std.mem.Allocator, body: []const u8, expected_count: return results; } - -/// Check if a string looks like a CUSIP (9 alphanumeric characters). -/// CUSIPs have 6 alphanumeric issuer chars + 2 issue chars + 1 check digit. -/// This is a heuristic — it won't catch all CUSIPs and may have false positives. -pub fn isCusipLike(s: []const u8) bool { - if (s.len != 9) return false; - // Must contain at least one digit (all-alpha would be a ticker) - var has_digit = false; - for (s) |c| { - if (!std.ascii.isAlphanumeric(c)) return false; - if (std.ascii.isDigit(c)) has_digit = true; - } - return has_digit; -} - -test "isCusipLike" { - try std.testing.expect(isCusipLike("02315N600")); // Vanguard Target 2035 - try std.testing.expect(isCusipLike("02315N709")); // Vanguard Target 2040 - try std.testing.expect(isCusipLike("459200101")); // IBM - try std.testing.expect(isCusipLike("06051XJ45")); // CD CUSIP - try std.testing.expect(!isCusipLike("AAPL")); // Too short - try std.testing.expect(!isCusipLike("ABCDEFGHI")); // No digits - try std.testing.expect(isCusipLike("NON40OR52")); // Looks cusip-like (has digits, 9 chars) - try std.testing.expect(!isCusipLike("12345")); // Too short -}