149 lines
6.8 KiB
Zig
149 lines
6.8 KiB
Zig
const std = @import("std");
|
|
const vaxis = @import("vaxis");
|
|
const zfin = @import("../root.zig");
|
|
const fmt = @import("../format.zig");
|
|
const theme_mod = @import("theme.zig");
|
|
const tui = @import("../tui.zig");
|
|
|
|
const App = tui.App;
|
|
const StyledLine = tui.StyledLine;
|
|
|
|
// ── Rendering ─────────────────────────────────────────────────
|
|
|
|
pub fn buildStyledLines(self: *App, arena: std.mem.Allocator) ![]const StyledLine {
|
|
const th = self.theme;
|
|
var lines: std.ArrayList(StyledLine) = .empty;
|
|
|
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
|
|
|
if (self.symbol.len == 0) {
|
|
try lines.append(arena, .{ .text = " No symbol selected. Press / to enter a symbol.", .style = th.mutedStyle() });
|
|
return lines.toOwnedSlice(arena);
|
|
}
|
|
|
|
if (self.candle_last_date) |d| {
|
|
var pdate_buf: [10]u8 = undefined;
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Trailing Returns: {s} (as of close on {s})", .{ self.symbol, d.format(&pdate_buf) }), .style = th.headerStyle() });
|
|
} else {
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Trailing Returns: {s}", .{self.symbol}), .style = th.headerStyle() });
|
|
}
|
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
|
|
|
if (self.trailing_price == null) {
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " No data. Run: zfin perf {s}", .{self.symbol}), .style = th.mutedStyle() });
|
|
return lines.toOwnedSlice(arena);
|
|
}
|
|
|
|
if (self.candle_count > 0) {
|
|
if (self.candle_first_date) |first| {
|
|
if (self.candle_last_date) |last| {
|
|
var fb: [10]u8 = undefined;
|
|
var lb: [10]u8 = undefined;
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Data: {d} points ({s} to {s})", .{
|
|
self.candle_count, first.format(&fb), last.format(&lb),
|
|
}), .style = th.mutedStyle() });
|
|
}
|
|
}
|
|
}
|
|
|
|
if (self.candles) |cc| {
|
|
if (cc.len > 0) {
|
|
var close_buf: [24]u8 = undefined;
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Latest close: {s}", .{fmt.fmtMoneyAbs(&close_buf, cc[cc.len - 1].close)}), .style = th.contentStyle() });
|
|
}
|
|
}
|
|
|
|
const has_total = self.trailing_total != null;
|
|
|
|
if (self.candle_last_date) |last| {
|
|
var db: [10]u8 = undefined;
|
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " As-of {s}:", .{last.format(&db)}), .style = th.headerStyle() });
|
|
}
|
|
try appendStyledReturnsTable(arena, &lines, self.trailing_price.?, if (has_total) self.trailing_total else null, th);
|
|
|
|
{
|
|
const today = fmt.todayDate();
|
|
const month_end = today.lastDayOfPriorMonth();
|
|
var db: [10]u8 = undefined;
|
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Month-end ({s}):", .{month_end.format(&db)}), .style = th.headerStyle() });
|
|
}
|
|
if (self.trailing_me_price) |me_price| {
|
|
try appendStyledReturnsTable(arena, &lines, me_price, if (has_total) self.trailing_me_total else null, th);
|
|
}
|
|
|
|
if (!has_total) {
|
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
|
try lines.append(arena, .{ .text = " (Set POLYGON_API_KEY for total returns with dividends)", .style = th.dimStyle() });
|
|
}
|
|
|
|
if (self.risk_metrics) |tr| {
|
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
|
try lines.append(arena, .{ .text = " Risk Metrics (monthly returns):", .style = th.headerStyle() });
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " {s:<20} {s:>14} {s:>14} {s:>14}", .{ "", "Volatility", "Sharpe", "Max DD" }), .style = th.mutedStyle() });
|
|
|
|
const risk_arr = [4]?zfin.risk.RiskMetrics{ tr.one_year, tr.three_year, tr.five_year, tr.ten_year };
|
|
const risk_labels = [4][]const u8{ "1-Year:", "3-Year:", "5-Year:", "10-Year:" };
|
|
|
|
for (0..4) |i| {
|
|
if (risk_arr[i]) |rm| {
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " {s:<20} {d:>13.1}% {d:>14.2} {d:>13.1}%", .{
|
|
risk_labels[i], rm.volatility * 100.0, rm.sharpe, rm.max_drawdown * 100.0,
|
|
}), .style = th.contentStyle() });
|
|
} else {
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " {s:<20} {s:>14} {s:>14} {s:>14}", .{
|
|
risk_labels[i], "—", "—", "—",
|
|
}), .style = th.mutedStyle() });
|
|
}
|
|
}
|
|
}
|
|
|
|
return lines.toOwnedSlice(arena);
|
|
}
|
|
|
|
fn appendStyledReturnsTable(
|
|
arena: std.mem.Allocator,
|
|
lines: *std.ArrayList(StyledLine),
|
|
price: zfin.performance.TrailingReturns,
|
|
total: ?zfin.performance.TrailingReturns,
|
|
th: theme_mod.Theme,
|
|
) !void {
|
|
const has_total = total != null;
|
|
if (has_total) {
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " {s:<20} {s:>14} {s:>14}", .{ "", "Price Only", "Total Return" }), .style = th.mutedStyle() });
|
|
} else {
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " {s:<20} {s:>14}", .{ "", "Price Only" }), .style = th.mutedStyle() });
|
|
}
|
|
|
|
const price_arr = [4]?zfin.performance.PerformanceResult{ price.one_year, price.three_year, price.five_year, price.ten_year };
|
|
const total_arr_vals: [4]?zfin.performance.PerformanceResult = if (total) |t|
|
|
.{ t.one_year, t.three_year, t.five_year, t.ten_year }
|
|
else
|
|
.{ null, null, null, null };
|
|
const labels = [4][]const u8{ "1-Year Return:", "3-Year Return:", "5-Year Return:", "10-Year Return:" };
|
|
const annualize = [4]bool{ false, true, true, true };
|
|
|
|
for (0..4) |i| {
|
|
var price_buf: [32]u8 = undefined;
|
|
var total_buf: [32]u8 = undefined;
|
|
const row = fmt.fmtReturnsRow(
|
|
&price_buf,
|
|
&total_buf,
|
|
price_arr[i],
|
|
if (has_total) total_arr_vals[i] else null,
|
|
annualize[i],
|
|
);
|
|
|
|
const row_style = if (price_arr[i] != null)
|
|
(if (row.price_positive) th.positiveStyle() else th.negativeStyle())
|
|
else
|
|
th.mutedStyle();
|
|
|
|
if (has_total) {
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " {s:<20} {s:>14} {s:>14}{s}", .{ labels[i], row.price_str, row.total_str orelse "N/A", row.suffix }), .style = row_style });
|
|
} else {
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " {s:<20} {s:>14}{s}", .{ labels[i], row.price_str, row.suffix }), .style = row_style });
|
|
}
|
|
}
|
|
}
|