migrate portfolio to new cli framework
This commit is contained in:
parent
2a89125977
commit
4cb5d1f711
2 changed files with 95 additions and 16 deletions
|
|
@ -1,11 +1,79 @@
|
|||
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");
|
||||
const views = @import("../views/portfolio_sections.zig");
|
||||
|
||||
pub fn run(io: std.Io, allocator: std.mem.Allocator, svc: *zfin.DataService, file_path: []const u8, watchlist_path: ?[]const u8, force_refresh: bool, as_of: zfin.Date, color: bool, out: *std.Io.Writer) !void {
|
||||
pub const ParsedArgs = struct {
|
||||
/// `--refresh`: invalidate the candle cache before loading prices,
|
||||
/// forcing a re-fetch from providers (server sync where configured,
|
||||
/// otherwise per-symbol provider calls).
|
||||
force_refresh: bool = false,
|
||||
};
|
||||
|
||||
pub const meta = struct {
|
||||
pub const name: []const u8 = "portfolio";
|
||||
pub const group: framework.Group = .portfolio;
|
||||
pub const synopsis: []const u8 = "Load and analyze the portfolio (positions + valuations + watchlist)";
|
||||
pub const help: []const u8 =
|
||||
\\Usage: zfin portfolio [--refresh]
|
||||
\\
|
||||
\\Load `portfolio.srf` (cwd → ZFIN_HOME), refresh per-symbol
|
||||
\\prices in parallel (server sync where ZFIN_SERVER is set,
|
||||
\\else providers), and print the position table + valuations
|
||||
\\+ historical-snapshot mini-tables. The watchlist (if
|
||||
\\`watchlist.srf` exists) is appended to the price-load step
|
||||
\\so its quotes show alongside.
|
||||
\\
|
||||
\\Options:
|
||||
\\ --refresh Force a re-fetch of every symbol's candles,
|
||||
\\ bypassing the per-symbol TTL freshness check.
|
||||
\\ Useful when you suspect cached data is wrong
|
||||
\\ or after rotating providers.
|
||||
\\
|
||||
;
|
||||
};
|
||||
|
||||
comptime {
|
||||
framework.validateCommandModule(@This());
|
||||
}
|
||||
|
||||
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
|
||||
var parsed: ParsedArgs = .{};
|
||||
for (cmd_args) |a| {
|
||||
if (std.mem.eql(u8, a, "--refresh")) {
|
||||
parsed.force_refresh = true;
|
||||
} else {
|
||||
try cli.stderrPrint(ctx.io, "Error: unexpected argument to 'portfolio': ");
|
||||
try cli.stderrPrint(ctx.io, a);
|
||||
try cli.stderrPrint(ctx.io, "\n");
|
||||
return error.UnexpectedArg;
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
pub fn run(ctx: *framework.RunCtx, parsed: 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;
|
||||
|
||||
const wl = ctx.resolveWatchlistPath();
|
||||
defer wl.deinit(allocator);
|
||||
const watchlist_path: ?[]const u8 =
|
||||
if (ctx.globals.watchlist_path != null or wl.resolved != null) wl.path else null;
|
||||
|
||||
const force_refresh = parsed.force_refresh;
|
||||
|
||||
// Load portfolio from SRF file
|
||||
var loaded = cli.loadPortfolio(io, allocator, file_path, as_of) orelse return;
|
||||
defer loaded.deinit(allocator);
|
||||
|
|
@ -803,3 +871,28 @@ test "display empty watchlist not shown" {
|
|||
// Watchlist header should NOT appear when there are no watch symbols
|
||||
try testing.expect(std.mem.indexOf(u8, out, "Watchlist") == null);
|
||||
}
|
||||
|
||||
// ── parseArgs tests ────────────────────────────────────────────
|
||||
|
||||
test "parseArgs: no args produces force_refresh=false" {
|
||||
var ctx: framework.RunCtx = undefined;
|
||||
ctx.io = std.testing.io;
|
||||
const args = [_][]const u8{};
|
||||
const parsed = try parseArgs(&ctx, &args);
|
||||
try std.testing.expect(!parsed.force_refresh);
|
||||
}
|
||||
|
||||
test "parseArgs: --refresh sets force_refresh" {
|
||||
var ctx: framework.RunCtx = undefined;
|
||||
ctx.io = std.testing.io;
|
||||
const args = [_][]const u8{"--refresh"};
|
||||
const parsed = try parseArgs(&ctx, &args);
|
||||
try std.testing.expect(parsed.force_refresh);
|
||||
}
|
||||
|
||||
test "parseArgs: unexpected args error" {
|
||||
var ctx: framework.RunCtx = undefined;
|
||||
ctx.io = std.testing.io;
|
||||
const args = [_][]const u8{"unexpected"};
|
||||
try std.testing.expectError(error.UnexpectedArg, parseArgs(&ctx, &args));
|
||||
}
|
||||
|
|
|
|||
16
src/main.zig
16
src/main.zig
|
|
@ -23,6 +23,7 @@ const command_modules = .{
|
|||
.etf = @import("commands/etf.zig"),
|
||||
|
||||
// Portfolio analysis
|
||||
.portfolio = @import("commands/portfolio.zig"),
|
||||
.milestones = @import("commands/milestones.zig"),
|
||||
|
||||
// Data hygiene
|
||||
|
|
@ -475,21 +476,6 @@ fn runCli(init: std.process.Init) !u8 {
|
|||
if (std.mem.eql(u8, command, "portfolio")) {
|
||||
// Parse --refresh flag; reject any other token (including old
|
||||
// positional FILE, which is now a global -p).
|
||||
var force_refresh = false;
|
||||
for (cmd_args) |a| {
|
||||
if (std.mem.eql(u8, a, "--refresh")) {
|
||||
force_refresh = true;
|
||||
} else {
|
||||
try reportUnexpectedArg(io, "portfolio", 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);
|
||||
const wl = resolveUserPath(io, allocator, config, globals.watchlist_path, zfin.Config.default_watchlist_filename);
|
||||
defer if (wl.resolved) |r| r.deinit(allocator);
|
||||
const wl_path: ?[]const u8 = if (globals.watchlist_path != null or wl.resolved != null) wl.path else null;
|
||||
try commands.portfolio.run(io, allocator, &svc, pf.path, wl_path, force_refresh, today, color, out);
|
||||
} else if (std.mem.eql(u8, command, "audit")) {
|
||||
const pf = resolveUserPath(io, allocator, config, globals.portfolio_path, zfin.Config.default_portfolio_filename);
|
||||
defer if (pf.resolved) |r| r.deinit(allocator);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue