migrate analysis to new cli framework
This commit is contained in:
parent
4cb5d1f711
commit
682ebd10c0
2 changed files with 73 additions and 17 deletions
|
|
@ -1,11 +1,56 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
const framework = @import("framework.zig");
|
||||
const fmt = cli.fmt;
|
||||
const Money = @import("../Money.zig");
|
||||
|
||||
pub const ParsedArgs = struct {};
|
||||
|
||||
pub const meta = struct {
|
||||
pub const name: []const u8 = "analysis";
|
||||
pub const group: framework.Group = .portfolio;
|
||||
pub const synopsis: []const u8 = "Show portfolio breakdowns by asset class, sector, geo, account, tax type";
|
||||
pub const help: []const u8 =
|
||||
\\Usage: zfin analysis
|
||||
\\
|
||||
\\Show portfolio analysis: equities/fixed-income split, plus
|
||||
\\block-bar breakdowns by asset class, sector, geographic
|
||||
\\region, account, and tax type. Reads classifications from
|
||||
\\`metadata.srf` and account tax types from `accounts.srf`
|
||||
\\(both in the same directory as the portfolio file).
|
||||
\\
|
||||
\\Run `zfin enrich <portfolio.srf> > metadata.srf` to bootstrap
|
||||
\\classifications, then edit by hand.
|
||||
\\
|
||||
;
|
||||
};
|
||||
|
||||
comptime {
|
||||
framework.validateCommandModule(@This());
|
||||
}
|
||||
|
||||
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
|
||||
if (cmd_args.len > 0) {
|
||||
try cli.stderrPrint(ctx.io, "Error: 'analysis' takes no arguments\n");
|
||||
return error.UnexpectedArg;
|
||||
}
|
||||
return .{};
|
||||
}
|
||||
|
||||
/// CLI `analysis` command: show portfolio analysis breakdowns.
|
||||
pub fn run(io: std.Io, allocator: std.mem.Allocator, svc: *zfin.DataService, file_path: []const u8, as_of: zfin.Date, color: bool, out: *std.Io.Writer) !void {
|
||||
pub fn run(ctx: *framework.RunCtx, _: ParsedArgs) !void {
|
||||
const svc = ctx.svc orelse return error.MissingDataService;
|
||||
const io = ctx.io;
|
||||
const allocator = ctx.allocator;
|
||||
const out = ctx.out;
|
||||
const color = ctx.color;
|
||||
const as_of = ctx.today;
|
||||
|
||||
const pf = ctx.resolvePortfolioPath();
|
||||
defer pf.deinit(allocator);
|
||||
const file_path = pf.path;
|
||||
|
||||
var loaded = cli.loadPortfolio(io, allocator, file_path, as_of) orelse return;
|
||||
defer loaded.deinit(allocator);
|
||||
|
||||
|
|
@ -13,16 +58,20 @@ pub fn run(io: std.Io, allocator: std.mem.Allocator, svc: *zfin.DataService, fil
|
|||
const positions = loaded.positions;
|
||||
const syms = loaded.syms;
|
||||
|
||||
// Build prices from cache
|
||||
// Refresh per-symbol prices via the parallel loader so analysis
|
||||
// works on TTL-fresh data by default. Previously this read
|
||||
// `getCachedCandles` directly, which silently used stale data
|
||||
// after long weekends or when the cache hadn't been refreshed.
|
||||
// The loader emits a stderr summary line ("Loaded N symbols
|
||||
// (X cached, Y server, Z provider)").
|
||||
var prices = std.StringHashMap(f64).init(allocator);
|
||||
defer prices.deinit();
|
||||
for (positions) |pos| {
|
||||
if (pos.shares <= 0) continue;
|
||||
if (svc.getCachedCandles(pos.symbol)) |cs| {
|
||||
defer cs.deinit();
|
||||
if (cs.data.len > 0) {
|
||||
try prices.put(pos.symbol, cs.data[cs.data.len - 1].close);
|
||||
}
|
||||
if (syms.len > 0) {
|
||||
var load_result = cli.loadPortfolioPrices(io, svc, syms, &.{}, false, color);
|
||||
defer load_result.deinit();
|
||||
var it = load_result.prices.iterator();
|
||||
while (it.next()) |entry| {
|
||||
try prices.put(entry.key_ptr.*, entry.value_ptr.*);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -150,6 +199,20 @@ pub fn printBreakdownSection(out: *std.Io.Writer, items: []const zfin.analysis.B
|
|||
|
||||
// ── Tests ────────────────────────────────────────────────────
|
||||
|
||||
test "parseArgs: no args produces empty ParsedArgs" {
|
||||
var ctx: framework.RunCtx = undefined;
|
||||
ctx.io = std.testing.io;
|
||||
const args = [_][]const u8{};
|
||||
_ = try parseArgs(&ctx, &args);
|
||||
}
|
||||
|
||||
test "parseArgs: any positional is rejected" {
|
||||
var ctx: framework.RunCtx = undefined;
|
||||
ctx.io = std.testing.io;
|
||||
const args = [_][]const u8{"unexpected"};
|
||||
try std.testing.expectError(error.UnexpectedArg, parseArgs(&ctx, &args));
|
||||
}
|
||||
|
||||
test "printBreakdownSection single item no color" {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var w: std.Io.Writer = .fixed(&buf);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ const command_modules = .{
|
|||
|
||||
// Portfolio analysis
|
||||
.portfolio = @import("commands/portfolio.zig"),
|
||||
.analysis = @import("commands/analysis.zig"),
|
||||
.milestones = @import("commands/milestones.zig"),
|
||||
|
||||
// Data hygiene
|
||||
|
|
@ -480,14 +481,6 @@ fn runCli(init: std.process.Init) !u8 {
|
|||
const pf = resolveUserPath(io, allocator, config, globals.portfolio_path, zfin.Config.default_portfolio_filename);
|
||||
defer if (pf.resolved) |r| r.deinit(allocator);
|
||||
try commands.audit.run(io, allocator, &svc, pf.path, cmd_args, today, now_s, color, out);
|
||||
} else if (std.mem.eql(u8, command, "analysis")) {
|
||||
for (cmd_args) |a| {
|
||||
try reportUnexpectedArg(io, "analysis", a);
|
||||
return 1;
|
||||
}
|
||||
const pf = resolveUserPath(io, allocator, config, globals.portfolio_path, zfin.Config.default_portfolio_filename);
|
||||
defer if (pf.resolved) |r| r.deinit(allocator);
|
||||
try commands.analysis.run(io, allocator, &svc, pf.path, today, color, out);
|
||||
} else if (std.mem.eql(u8, command, "projections")) {
|
||||
var events_enabled = true;
|
||||
var as_of: ?zfin.Date = null;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue