move isCusipLike to a more generic place

This commit is contained in:
Emil Lerch 2026-03-01 12:45:26 -08:00
parent 9d86ccf2e5
commit 6d9374ab77
Signed by: lobo
GPG key ID: A7B62D657EF764F8
5 changed files with 35 additions and 39 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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",

View file

@ -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
}