migrate analysis to new cli framework
This commit is contained in:
parent
9f7e77dec9
commit
a4be58fa05
2 changed files with 73 additions and 17 deletions
|
|
@ -1,11 +1,56 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const zfin = @import("../root.zig");
|
const zfin = @import("../root.zig");
|
||||||
const cli = @import("common.zig");
|
const cli = @import("common.zig");
|
||||||
|
const framework = @import("framework.zig");
|
||||||
const fmt = cli.fmt;
|
const fmt = cli.fmt;
|
||||||
const Money = @import("../Money.zig");
|
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.
|
/// 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;
|
var loaded = cli.loadPortfolio(io, allocator, file_path, as_of) orelse return;
|
||||||
defer loaded.deinit(allocator);
|
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 positions = loaded.positions;
|
||||||
const syms = loaded.syms;
|
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);
|
var prices = std.StringHashMap(f64).init(allocator);
|
||||||
defer prices.deinit();
|
defer prices.deinit();
|
||||||
for (positions) |pos| {
|
if (syms.len > 0) {
|
||||||
if (pos.shares <= 0) continue;
|
var load_result = cli.loadPortfolioPrices(io, svc, syms, &.{}, false, color);
|
||||||
if (svc.getCachedCandles(pos.symbol)) |cs| {
|
defer load_result.deinit();
|
||||||
defer cs.deinit();
|
var it = load_result.prices.iterator();
|
||||||
if (cs.data.len > 0) {
|
while (it.next()) |entry| {
|
||||||
try prices.put(pos.symbol, cs.data[cs.data.len - 1].close);
|
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 ────────────────────────────────────────────────────
|
// ── 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" {
|
test "printBreakdownSection single item no color" {
|
||||||
var buf: [4096]u8 = undefined;
|
var buf: [4096]u8 = undefined;
|
||||||
var w: std.Io.Writer = .fixed(&buf);
|
var w: std.Io.Writer = .fixed(&buf);
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ const command_modules = .{
|
||||||
|
|
||||||
// Portfolio analysis
|
// Portfolio analysis
|
||||||
.portfolio = @import("commands/portfolio.zig"),
|
.portfolio = @import("commands/portfolio.zig"),
|
||||||
|
.analysis = @import("commands/analysis.zig"),
|
||||||
.milestones = @import("commands/milestones.zig"),
|
.milestones = @import("commands/milestones.zig"),
|
||||||
|
|
||||||
// Data hygiene
|
// 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);
|
const pf = resolveUserPath(io, allocator, config, globals.portfolio_path, zfin.Config.default_portfolio_filename);
|
||||||
defer if (pf.resolved) |r| r.deinit(allocator);
|
defer if (pf.resolved) |r| r.deinit(allocator);
|
||||||
try commands.audit.run(io, allocator, &svc, pf.path, cmd_args, today, now_s, color, out);
|
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")) {
|
} else if (std.mem.eql(u8, command, "projections")) {
|
||||||
var events_enabled = true;
|
var events_enabled = true;
|
||||||
var as_of: ?zfin.Date = null;
|
var as_of: ?zfin.Date = null;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue