zfin/src/commands/splits.zig

138 lines
4.9 KiB
Zig

const std = @import("std");
const zfin = @import("../root.zig");
const cli = @import("common.zig");
const framework = @import("framework.zig");
const fmt = cli.fmt;
pub const ParsedArgs = struct {
symbol: []const u8,
};
pub const meta: framework.Meta = .{
.name = "splits",
.group = .symbol_lookup,
.synopsis = "Show split history for a symbol",
.uppercase_first_arg = true,
.help =
\\Usage: zfin splits <SYMBOL>
\\
\\Show the split history for a symbol from Polygon.io. Cached
\\for 14 days; subsequent calls within that window serve the
\\cached data without an API call.
\\
\\Examples:
\\ zfin splits AAPL # 4:1 (2020-08-31), 7:1 (2014-06-09)
\\ zfin splits NVDA # 10:1 (2024-06-10), 4:1 (2021-07-20), ...
\\
,
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
if (cmd_args.len < 1) {
try cli.stderrPrint(ctx.io, "Error: 'splits' requires a symbol argument\n");
return error.MissingSymbol;
}
if (cmd_args.len > 1) {
try cli.stderrPrint(ctx.io, "Error: 'splits' takes a single symbol argument\n");
return error.UnexpectedArg;
}
return .{ .symbol = cmd_args[0] };
}
pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
const svc = ctx.svc orelse return error.MissingDataService;
const result = svc.getSplits(parsed.symbol) catch |err| switch (err) {
zfin.DataError.NoApiKey => {
try cli.stderrPrint(ctx.io, "Error: POLYGON_API_KEY not set. Get a free key at https://polygon.io\n");
return;
},
else => {
try cli.stderrPrint(ctx.io, "Error fetching split data.\n");
return;
},
};
defer result.deinit();
if (result.source == .cached) try cli.stderrPrint(ctx.io, "(using cached split data)\n");
try display(result.data, parsed.symbol, ctx.color, ctx.out);
}
pub fn display(splits: []const zfin.Split, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
try cli.printBold(out, color, "\nSplit History for {s}\n", .{symbol});
try out.print("========================================\n", .{});
if (splits.len == 0) {
try cli.printFg(out, color, cli.CLR_MUTED, " No splits found.\n\n", .{});
return;
}
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print("{s:>12} {s:>10}\n", .{ "Date", "Ratio" });
try out.print("{s:->12} {s:->10}\n", .{ "", "" });
try cli.reset(out, color);
for (splits) |s| {
try out.print("{f} {d:.0}:{d:.0}\n", .{ s.date.padLeft(12), s.numerator, s.denominator });
}
try out.print("\n{d} split(s)\n\n", .{splits.len});
}
// ── Tests ────────────────────────────────────────────────────
test "parseArgs: accepts a single symbol" {
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.symbol);
}
test "parseArgs: missing symbol errors" {
var ctx: framework.RunCtx = undefined;
ctx.io = std.testing.io;
const args = [_][]const u8{};
try std.testing.expectError(error.MissingSymbol, 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));
}
test "display shows split data" {
var buf: [4096]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
const splits = [_]zfin.Split{
.{ .date = .{ .days = 18000 }, .numerator = 4, .denominator = 1 },
.{ .date = .{ .days = 15000 }, .numerator = 7, .denominator = 1 },
};
try display(&splits, "AAPL", false, &w);
const out = w.buffered();
try std.testing.expect(std.mem.indexOf(u8, out, "AAPL") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "4:1") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "7:1") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "2 split(s)") != null);
}
test "display shows empty message" {
var buf: [4096]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
const splits = [_]zfin.Split{};
try display(&splits, "BRK.A", false, &w);
const out = w.buffered();
try std.testing.expect(std.mem.indexOf(u8, out, "No splits found") != null);
}
test "display no ANSI without color" {
var buf: [4096]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
const splits = [_]zfin.Split{
.{ .date = .{ .days = 18000 }, .numerator = 2, .denominator = 1 },
};
try display(&splits, "GOOG", false, &w);
const out = w.buffered();
try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null);
}