164 lines
6.5 KiB
Zig
164 lines
6.5 KiB
Zig
const std = @import("std");
|
|
const zfin = @import("../root.zig");
|
|
const cli = @import("common.zig");
|
|
const fmt = cli.fmt;
|
|
|
|
pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, ntm: usize, color: bool, out: *std.Io.Writer) !void {
|
|
const result = svc.getOptions(symbol) catch |err| switch (err) {
|
|
zfin.DataError.FetchFailed => {
|
|
try cli.stderrPrint("Error fetching options data from CBOE.\n");
|
|
return;
|
|
},
|
|
else => {
|
|
try cli.stderrPrint("Error loading options data.\n");
|
|
return;
|
|
},
|
|
};
|
|
const ch = result.data;
|
|
defer {
|
|
for (ch) |chain| {
|
|
allocator.free(chain.underlying_symbol);
|
|
allocator.free(chain.calls);
|
|
allocator.free(chain.puts);
|
|
}
|
|
allocator.free(ch);
|
|
}
|
|
|
|
if (result.source == .cached) try cli.stderrPrint("(using cached options data)\n");
|
|
|
|
if (ch.len == 0) {
|
|
try cli.stderrPrint("No options data found.\n");
|
|
return;
|
|
}
|
|
|
|
try display(out, allocator, ch, symbol, ntm, color);
|
|
}
|
|
|
|
pub fn display(out: *std.Io.Writer, allocator: std.mem.Allocator, chains: []const zfin.OptionsChain, symbol: []const u8, ntm: usize, color: bool) !void {
|
|
if (chains.len == 0) return;
|
|
|
|
try cli.setBold(out, color);
|
|
try out.print("\nOptions Chain for {s}\n", .{symbol});
|
|
try cli.reset(out, color);
|
|
try out.print("========================================\n", .{});
|
|
if (chains[0].underlying_price) |price| {
|
|
var price_buf: [24]u8 = undefined;
|
|
try out.print("Underlying: {s} {d} expiration(s) +/- {d} strikes NTM\n", .{ fmt.fmtMoney(&price_buf, price), chains.len, ntm });
|
|
} else {
|
|
try out.print("{d} expiration(s) available\n", .{chains.len});
|
|
}
|
|
|
|
// Find nearest monthly expiration to auto-expand
|
|
var auto_expand_idx: ?usize = null;
|
|
for (chains, 0..) |chain, ci| {
|
|
if (fmt.isMonthlyExpiration(chain.expiration)) {
|
|
auto_expand_idx = ci;
|
|
break;
|
|
}
|
|
}
|
|
// If no monthly found, expand the first one
|
|
if (auto_expand_idx == null and chains.len > 0) auto_expand_idx = 0;
|
|
|
|
const atm_price = if (chains[0].underlying_price) |p| p else @as(f64, 0);
|
|
|
|
// List all expirations, expanding the nearest monthly
|
|
for (chains, 0..) |chain, ci| {
|
|
var db: [10]u8 = undefined;
|
|
const is_monthly = fmt.isMonthlyExpiration(chain.expiration);
|
|
const is_expanded = auto_expand_idx != null and ci == auto_expand_idx.?;
|
|
|
|
try out.print("\n", .{});
|
|
if (is_expanded) {
|
|
try cli.setBold(out, color);
|
|
try out.print("{s} ({d} calls, {d} puts)", .{
|
|
chain.expiration.format(&db), chain.calls.len, chain.puts.len,
|
|
});
|
|
if (is_monthly) try out.print(" [monthly]", .{});
|
|
try cli.reset(out, color);
|
|
try out.print("\n", .{});
|
|
|
|
// Print calls
|
|
try printSection(out, allocator, "CALLS", chain.calls, atm_price, ntm, true, color);
|
|
try out.print("\n", .{});
|
|
// Print puts
|
|
try printSection(out, allocator, "PUTS", chain.puts, atm_price, ntm, false, color);
|
|
} else {
|
|
try cli.setFg(out, color, if (is_monthly) cli.CLR_HEADER else cli.CLR_MUTED);
|
|
try out.print("{s} ({d} calls, {d} puts)", .{
|
|
chain.expiration.format(&db), chain.calls.len, chain.puts.len,
|
|
});
|
|
if (is_monthly) try out.print(" [monthly]", .{});
|
|
try cli.reset(out, color);
|
|
try out.print("\n", .{});
|
|
}
|
|
}
|
|
|
|
try out.print("\n", .{});
|
|
}
|
|
|
|
pub fn printSection(
|
|
out: *std.Io.Writer,
|
|
allocator: std.mem.Allocator,
|
|
label: []const u8,
|
|
contracts: []const zfin.OptionContract,
|
|
atm_price: f64,
|
|
ntm: usize,
|
|
is_calls: bool,
|
|
color: bool,
|
|
) !void {
|
|
try cli.setBold(out, color);
|
|
try out.print(" {s}\n", .{label});
|
|
try cli.reset(out, color);
|
|
try cli.setFg(out, color, cli.CLR_MUTED);
|
|
try out.print(" {s:>10} {s:>10} {s:>10} {s:>10} {s:>10} {s:>8} {s:>8}\n", .{
|
|
"Strike", "Last", "Bid", "Ask", "Volume", "OI", "IV",
|
|
});
|
|
try out.print(" {s:->10} {s:->10} {s:->10} {s:->10} {s:->10} {s:->8} {s:->8}\n", .{
|
|
"", "", "", "", "", "", "",
|
|
});
|
|
try cli.reset(out, color);
|
|
|
|
const filtered = fmt.filterNearMoney(contracts, atm_price, ntm);
|
|
for (filtered) |c| {
|
|
const itm = if (is_calls) c.strike <= atm_price else c.strike >= atm_price;
|
|
const prefix: []const u8 = if (itm) " |" else " ";
|
|
const line = try fmt.fmtContractLine(allocator, prefix, c);
|
|
defer allocator.free(line);
|
|
try out.print("{s}\n", .{line});
|
|
}
|
|
}
|
|
|
|
// ── Tests ────────────────────────────────────────────────────
|
|
|
|
test "printSection shows header and contracts" {
|
|
var buf: [8192]u8 = undefined;
|
|
var w: std.Io.Writer = .fixed(&buf);
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const calls = [_]zfin.OptionContract{
|
|
.{ .contract_type = .call, .strike = 150.0, .expiration = .{ .days = 20100 }, .bid = 5.0, .ask = 5.50, .last_price = 5.25 },
|
|
.{ .contract_type = .call, .strike = 155.0, .expiration = .{ .days = 20100 }, .bid = 2.0, .ask = 2.50, .last_price = 2.25 },
|
|
};
|
|
try printSection(&w, gpa.allocator(), "CALLS", &calls, 152.0, 8, true, false);
|
|
const out = w.buffered();
|
|
try std.testing.expect(std.mem.indexOf(u8, out, "CALLS") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, out, "Strike") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null);
|
|
}
|
|
|
|
test "display shows chain header no color" {
|
|
var buf: [8192]u8 = undefined;
|
|
var w: std.Io.Writer = .fixed(&buf);
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const calls = [_]zfin.OptionContract{};
|
|
const puts = [_]zfin.OptionContract{};
|
|
const chains = [_]zfin.OptionsChain{
|
|
.{ .underlying_symbol = "SPY", .underlying_price = 500.0, .expiration = .{ .days = 20100 }, .calls = &calls, .puts = &puts },
|
|
};
|
|
try display(&w, gpa.allocator(), &chains, "SPY", 8, false);
|
|
const out = w.buffered();
|
|
try std.testing.expect(std.mem.indexOf(u8, out, "Options Chain for SPY") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, out, "1 expiration(s)") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null);
|
|
}
|