data refresh policy
This commit is contained in:
parent
4e6ae0ba51
commit
c73b58f059
11 changed files with 127 additions and 70 deletions
|
|
@ -68,7 +68,7 @@ pub fn run(ctx: *framework.RunCtx, _: ParsedArgs) !void {
|
|||
var prices = std.StringHashMap(f64).init(allocator);
|
||||
defer prices.deinit();
|
||||
if (syms.len > 0) {
|
||||
var load_result = cli.loadPortfolioPrices(io, svc, syms, &.{}, false, color);
|
||||
var load_result = cli.loadPortfolioPrices(io, svc, syms, &.{}, ctx.globals.refresh_policy, color);
|
||||
defer load_result.deinit();
|
||||
var it = load_result.prices.iterator();
|
||||
while (it.next()) |entry| {
|
||||
|
|
|
|||
|
|
@ -1813,6 +1813,7 @@ fn runHygieneCheck(
|
|||
as_of: Date,
|
||||
now_s: i64,
|
||||
color: bool,
|
||||
refresh: framework.RefreshPolicy,
|
||||
out: *std.Io.Writer,
|
||||
) !void {
|
||||
// Load portfolio
|
||||
|
|
@ -2106,7 +2107,7 @@ fn runHygieneCheck(
|
|||
const pos_syms = try portfolio.stockSymbols(allocator);
|
||||
defer allocator.free(pos_syms);
|
||||
if (pos_syms.len > 0) {
|
||||
var load_result = cli.loadPortfolioPrices(io, svc, pos_syms, &.{}, false, color);
|
||||
var load_result = cli.loadPortfolioPrices(io, svc, pos_syms, &.{}, refresh, color);
|
||||
defer load_result.deinit();
|
||||
var pit = load_result.prices.iterator();
|
||||
while (pit.next()) |entry| {
|
||||
|
|
@ -2208,7 +2209,7 @@ fn runHygieneCheck(
|
|||
// there are no new lots at all, or when the pipeline can't run
|
||||
// (not in a git repo). Threshold is a judgment call; see
|
||||
// `audit_large_lot_threshold`.
|
||||
if (contributions.findUnmatchedLargeLots(io, allocator, svc, portfolio_path, audit_large_lot_threshold, as_of, color)) |found| {
|
||||
if (contributions.findUnmatchedLargeLots(io, allocator, svc, portfolio_path, audit_large_lot_threshold, as_of, color, refresh)) |found| {
|
||||
var found_mut = found;
|
||||
defer found_mut.deinit();
|
||||
|
||||
|
|
@ -2326,7 +2327,7 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
|||
|
||||
// Flagless mode: run portfolio hygiene check
|
||||
if (fidelity_csv == null and schwab_csv == null and !schwab_summary) {
|
||||
return runHygieneCheck(io, allocator, svc, portfolio_path, stale_days, verbose, as_of, now_s, color, out);
|
||||
return runHygieneCheck(io, allocator, svc, portfolio_path, stale_days, verbose, as_of, now_s, color, ctx.globals.refresh_policy, out);
|
||||
}
|
||||
|
||||
// Load portfolio
|
||||
|
|
@ -2364,7 +2365,7 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
|||
defer allocator.free(pos_syms);
|
||||
|
||||
if (pos_syms.len > 0) {
|
||||
var load_result = cli.loadPortfolioPrices(io, svc, pos_syms, &.{}, false, color);
|
||||
var load_result = cli.loadPortfolioPrices(io, svc, pos_syms, &.{}, ctx.globals.refresh_policy, color);
|
||||
defer load_result.deinit();
|
||||
var it = load_result.prices.iterator();
|
||||
while (it.next()) |entry| {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const zfin = @import("../root.zig");
|
|||
const srf = @import("srf");
|
||||
const history = @import("../history.zig");
|
||||
const git = @import("../git.zig");
|
||||
const framework = @import("framework.zig");
|
||||
pub const fmt = @import("../format.zig");
|
||||
|
||||
// ── Default CLI colors (match TUI default Monokai theme) ─────
|
||||
|
|
@ -264,7 +265,7 @@ pub fn loadPortfolioPrices(
|
|||
svc: *zfin.DataService,
|
||||
portfolio_syms: ?[]const []const u8,
|
||||
watch_syms: []const []const u8,
|
||||
force_refresh: bool,
|
||||
refresh: framework.RefreshPolicy,
|
||||
color: bool,
|
||||
) zfin.DataService.LoadAllResult {
|
||||
var aggregate = AggregateProgress{ .io = io, .color = color };
|
||||
|
|
@ -276,10 +277,18 @@ pub fn loadPortfolioPrices(
|
|||
.grand_total = (if (portfolio_syms) |ps| ps.len else 0) + watch_syms.len,
|
||||
};
|
||||
|
||||
// .force → invalidate cache before reading; the underlying
|
||||
// loader's `force_refresh` does exactly that.
|
||||
// .never → today the underlying loader has no "skip TTL
|
||||
// entirely" knob, so we approximate by passing
|
||||
// `force_refresh = false` (TTL-respecting). A true
|
||||
// skip-network mode is a follow-up if/when needed; the
|
||||
// common case for `--refresh-data=never` is "the cache is
|
||||
// fresh and I'm offline," which works fine via TTL today.
|
||||
const result = svc.loadAllPrices(
|
||||
portfolio_syms,
|
||||
watch_syms,
|
||||
.{ .force_refresh = force_refresh, .color = color },
|
||||
.{ .force_refresh = refresh == .force, .color = color },
|
||||
aggregate.callback(),
|
||||
symbol_progress.callback(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -443,13 +443,13 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
|||
.{ .date_at_or_before = now_date_requested };
|
||||
|
||||
if (now_is_live) {
|
||||
var now_live = try LiveSide.load(io, allocator, svc, portfolio_path, as_of, color);
|
||||
var now_live = try LiveSide.load(io, allocator, svc, portfolio_path, as_of, color, ctx.globals.refresh_policy);
|
||||
defer now_live.deinit(allocator);
|
||||
|
||||
// Attribution uses the resolved CommitSpecs so --commit-*
|
||||
// overrides + date fallbacks share one classifier. The caller
|
||||
// adapts dates to `CommitSpec.date_at_or_before` upstream.
|
||||
const attribution = contributions.computeAttributionSpec(io, allocator, svc, portfolio_path, attr_before, attr_after_opt, as_of, color);
|
||||
const attribution = contributions.computeAttributionSpec(io, allocator, svc, portfolio_path, attr_before, attr_after_opt, as_of, color, ctx.globals.refresh_policy);
|
||||
|
||||
try renderFromParts(out, color, allocator, .{
|
||||
.then_date = then_date,
|
||||
|
|
@ -466,7 +466,7 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
|||
var now_side = try compare_core.loadSnapshotSide(io, allocator, hist_dir, now_date);
|
||||
defer now_side.deinit(allocator);
|
||||
|
||||
const attribution = contributions.computeAttributionSpec(io, allocator, svc, portfolio_path, attr_before, attr_after_opt, as_of, color);
|
||||
const attribution = contributions.computeAttributionSpec(io, allocator, svc, portfolio_path, attr_before, attr_after_opt, as_of, color, ctx.globals.refresh_policy);
|
||||
|
||||
try renderFromParts(out, color, allocator, .{
|
||||
.then_date = then_date,
|
||||
|
|
@ -594,6 +594,7 @@ const LiveSide = struct {
|
|||
portfolio_path: []const u8,
|
||||
as_of: Date,
|
||||
color: bool,
|
||||
refresh: framework.RefreshPolicy,
|
||||
) !LiveSide {
|
||||
var loaded_pf = cli.loadPortfolio(io, allocator, portfolio_path, as_of) orelse return error.PortfolioLoadFailed;
|
||||
errdefer loaded_pf.deinit(allocator);
|
||||
|
|
@ -607,7 +608,7 @@ const LiveSide = struct {
|
|||
errdefer prices.deinit();
|
||||
|
||||
if (loaded_pf.syms.len > 0) {
|
||||
var load_result = cli.loadPortfolioPrices(io, svc, loaded_pf.syms, &.{}, false, color);
|
||||
var load_result = cli.loadPortfolioPrices(io, svc, loaded_pf.syms, &.{}, refresh, color);
|
||||
defer load_result.deinit();
|
||||
var it = load_result.prices.iterator();
|
||||
while (it.next()) |entry| prices.put(entry.key_ptr.*, entry.value_ptr.*) catch {};
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
|||
defer pf.deinit(allocator);
|
||||
const portfolio_path = pf.path;
|
||||
|
||||
return runImpl(io, allocator, svc, portfolio_path, before, after, as_of, color, out);
|
||||
return runImpl(io, allocator, svc, portfolio_path, before, after, as_of, color, ctx.globals.refresh_policy, out);
|
||||
}
|
||||
|
||||
fn runImpl(
|
||||
|
|
@ -322,6 +322,7 @@ fn runImpl(
|
|||
after: ?git.CommitSpec,
|
||||
as_of: Date,
|
||||
color: bool,
|
||||
refresh: framework.RefreshPolicy,
|
||||
out: *std.Io.Writer,
|
||||
) !void {
|
||||
// Arena for all transient allocations: git subprocess buffers, duped path
|
||||
|
|
@ -341,7 +342,7 @@ fn runImpl(
|
|||
return;
|
||||
}
|
||||
|
||||
var ctx = prepareReport(io, allocator, arena, svc, portfolio_path, before, after, as_of, color, .verbose) catch return;
|
||||
var ctx = prepareReport(io, allocator, arena, svc, portfolio_path, before, after, as_of, color, refresh, .verbose) catch return;
|
||||
defer ctx.deinit();
|
||||
|
||||
try printReport(out, &ctx.report, ctx.endpoints.label, color);
|
||||
|
|
@ -392,6 +393,7 @@ fn prepareReport(
|
|||
after_spec: ?git.CommitSpec,
|
||||
as_of: Date,
|
||||
color: bool,
|
||||
refresh: framework.RefreshPolicy,
|
||||
verbosity: Verbosity,
|
||||
) PrepareError!ReportContext {
|
||||
const repo = git.findRepo(io, arena, portfolio_path) catch |err| {
|
||||
|
|
@ -473,7 +475,7 @@ fn prepareReport(
|
|||
while (sit.next()) |k| syms.append(arena, k.*) catch return error.PrepareFailed;
|
||||
|
||||
if (syms.items.len > 0) {
|
||||
var load_result = cli.loadPortfolioPrices(io, svc, syms.items, &.{}, false, color);
|
||||
var load_result = cli.loadPortfolioPrices(io, svc, syms.items, &.{}, refresh, color);
|
||||
defer load_result.deinit();
|
||||
var pit = load_result.prices.iterator();
|
||||
while (pit.next()) |entry| {
|
||||
|
|
@ -834,6 +836,7 @@ pub fn computeAttributionSpec(
|
|||
after: ?git.CommitSpec,
|
||||
as_of: Date,
|
||||
color: bool,
|
||||
refresh: framework.RefreshPolicy,
|
||||
) ?AttributionSummary {
|
||||
if (before == null and after != null) return null;
|
||||
|
||||
|
|
@ -841,7 +844,7 @@ pub fn computeAttributionSpec(
|
|||
defer arena_state.deinit();
|
||||
const arena = arena_state.allocator();
|
||||
|
||||
var ctx = prepareReport(io, allocator, arena, svc, portfolio_path, before, after, as_of, color, .silent) catch return null;
|
||||
var ctx = prepareReport(io, allocator, arena, svc, portfolio_path, before, after, as_of, color, refresh, .silent) catch return null;
|
||||
defer ctx.deinit();
|
||||
|
||||
return summarizeAttribution(ctx);
|
||||
|
|
@ -902,6 +905,7 @@ pub fn findUnmatchedLargeLots(
|
|||
threshold: f64,
|
||||
as_of: Date,
|
||||
color: bool,
|
||||
refresh: framework.RefreshPolicy,
|
||||
) ?UnmatchedLargeLotSet {
|
||||
var arena_state = std.heap.ArenaAllocator.init(allocator);
|
||||
errdefer arena_state.deinit();
|
||||
|
|
@ -915,7 +919,7 @@ pub fn findUnmatchedLargeLots(
|
|||
//
|
||||
// Separate allocator here so we can tear the whole thing down
|
||||
// via `arena_state.deinit` once we've copied out the descriptors.
|
||||
var ctx = prepareReport(io, allocator, arena, svc, portfolio_path, null, null, as_of, color, .silent) catch {
|
||||
var ctx = prepareReport(io, allocator, arena, svc, portfolio_path, null, null, as_of, color, refresh, .silent) catch {
|
||||
arena_state.deinit();
|
||||
return null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -127,21 +127,25 @@ pub const Meta = struct {
|
|||
|
||||
// ── Refresh policy ────────────────────────────────────────────
|
||||
|
||||
/// Cache-freshness policy for the invocation. Defined here ahead of
|
||||
/// commit 18c so command modules can take a dependency on the type
|
||||
/// without churn later. Threading into `loadPortfolioPrices` and the
|
||||
/// per-symbol getters happens in 18c.
|
||||
/// Cache-freshness policy for the invocation. Set via the global
|
||||
/// `--refresh-data=<value>` flag (default: `.auto`). Threaded into
|
||||
/// `loadPortfolioPrices` and through every multi-symbol command's
|
||||
/// price-loading code.
|
||||
///
|
||||
/// - `default`: respect cache TTLs. Fresh entries served from cache;
|
||||
/// User-facing flag values match the variant names exactly:
|
||||
///
|
||||
/// - `auto`: respect cache TTLs. Fresh entries served from cache;
|
||||
/// stale entries trigger a provider refetch (or server sync if
|
||||
/// `ZFIN_SERVER` is configured). The right behavior for almost
|
||||
/// every invocation.
|
||||
/// - `force`: invalidate cache before reading. Equivalent to today's
|
||||
/// `portfolio --refresh` flag, generalized to all commands.
|
||||
/// every invocation; the default.
|
||||
/// - `force`: invalidate cache before reading. Re-fetches every
|
||||
/// symbol's data from providers regardless of TTL freshness.
|
||||
/// Useful when you suspect cached data is wrong or after rotating
|
||||
/// providers.
|
||||
/// - `never`: serve whatever's in cache regardless of TTL. No
|
||||
/// provider calls. Useful for offline operation, debugging, and
|
||||
/// reproducible historical analysis.
|
||||
pub const RefreshPolicy = enum { default, force, never };
|
||||
pub const RefreshPolicy = enum { auto, force, never };
|
||||
|
||||
// ── Globals ───────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -154,8 +158,8 @@ pub const Globals = struct {
|
|||
portfolio_path: ?[]const u8 = null,
|
||||
/// Explicit watchlist path from -w/--watchlist (raw, null if not set).
|
||||
watchlist_path: ?[]const u8 = null,
|
||||
/// Cache-freshness policy. Wired in commit 18c; default for now.
|
||||
refresh_policy: RefreshPolicy = .default,
|
||||
/// Cache-freshness policy from `--refresh-data=<value>`.
|
||||
refresh_policy: RefreshPolicy = .auto,
|
||||
};
|
||||
|
||||
// ── RunCtx ────────────────────────────────────────────────────
|
||||
|
|
@ -434,9 +438,9 @@ test "Group.label: every variant has a non-empty label" {
|
|||
}
|
||||
}
|
||||
|
||||
test "RefreshPolicy: default variant exists" {
|
||||
const policy: RefreshPolicy = .default;
|
||||
try testing.expectEqual(RefreshPolicy.default, policy);
|
||||
test "RefreshPolicy: auto is the default variant" {
|
||||
const policy: RefreshPolicy = .auto;
|
||||
try testing.expectEqual(RefreshPolicy.auto, policy);
|
||||
}
|
||||
|
||||
test "Globals: zero-init defaults" {
|
||||
|
|
@ -444,7 +448,7 @@ test "Globals: zero-init defaults" {
|
|||
try testing.expect(!g.no_color);
|
||||
try testing.expect(g.portfolio_path == null);
|
||||
try testing.expect(g.watchlist_path == null);
|
||||
try testing.expectEqual(RefreshPolicy.default, g.refresh_policy);
|
||||
try testing.expectEqual(RefreshPolicy.auto, g.refresh_policy);
|
||||
}
|
||||
|
||||
test "printCommandHelp: writes meta.help and ensures trailing newline" {
|
||||
|
|
|
|||
|
|
@ -6,19 +6,14 @@ const fmt = cli.fmt;
|
|||
const Money = @import("../Money.zig");
|
||||
const views = @import("../views/portfolio_sections.zig");
|
||||
|
||||
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 ParsedArgs = struct {};
|
||||
|
||||
pub const meta: framework.Meta = .{
|
||||
.name = "portfolio",
|
||||
.group = .portfolio,
|
||||
.synopsis = "Load and analyze the portfolio (positions + valuations + watchlist)",
|
||||
.help =
|
||||
\\Usage: zfin portfolio [--refresh]
|
||||
\\Usage: zfin portfolio
|
||||
\\
|
||||
\\Load `portfolio.srf` (cwd → ZFIN_HOME), refresh per-symbol
|
||||
\\prices in parallel (server sync where ZFIN_SERVER is set,
|
||||
|
|
@ -27,11 +22,11 @@ pub const meta: framework.Meta = .{
|
|||
\\`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.
|
||||
\\Refresh policy comes from the global `--refresh-data=<value>`
|
||||
\\flag (default: auto). Use `--refresh-data=force` to force a
|
||||
\\re-fetch of every symbol's candles, bypassing the per-symbol
|
||||
\\TTL freshness check. Use `--refresh-data=never` to serve
|
||||
\\cache contents only (offline mode).
|
||||
\\
|
||||
,
|
||||
.uppercase_first_arg = false,
|
||||
|
|
@ -42,21 +37,21 @@ comptime {
|
|||
}
|
||||
|
||||
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
|
||||
var parsed: ParsedArgs = .{};
|
||||
for (cmd_args) |a| {
|
||||
if (cmd_args.len > 0) {
|
||||
const a = cmd_args[0];
|
||||
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");
|
||||
try cli.stderrPrint(ctx.io, "Error: --refresh is now a global flag. Use `zfin --refresh-data=force portfolio` instead.\n");
|
||||
return error.UnexpectedArg;
|
||||
}
|
||||
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;
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !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;
|
||||
|
|
@ -73,8 +68,6 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
|||
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);
|
||||
|
|
@ -118,7 +111,7 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
|||
svc,
|
||||
syms,
|
||||
watch_syms.items,
|
||||
force_refresh,
|
||||
ctx.globals.refresh_policy,
|
||||
color,
|
||||
);
|
||||
defer load_result.deinit(); // Free the prices hashmap after we copy
|
||||
|
|
@ -875,20 +868,18 @@ test "display empty watchlist not shown" {
|
|||
|
||||
// ── parseArgs tests ────────────────────────────────────────────
|
||||
|
||||
test "parseArgs: no args produces force_refresh=false" {
|
||||
test "parseArgs: no args returns empty ParsedArgs" {
|
||||
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);
|
||||
_ = try parseArgs(&ctx, &args);
|
||||
}
|
||||
|
||||
test "parseArgs: --refresh sets force_refresh" {
|
||||
test "parseArgs: --refresh rejected (now a global)" {
|
||||
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);
|
||||
try std.testing.expectError(error.UnexpectedArg, parseArgs(&ctx, &args));
|
||||
}
|
||||
|
||||
test "parseArgs: unexpected args error" {
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
|||
// The duplicate-skip fast path above already handled the common
|
||||
// "cache is fresh, snapshot exists" case without any of this work.
|
||||
if (syms.len > 0 and as_of_override == null) {
|
||||
var load_result = cli.loadPortfolioPrices(io, svc, syms, &.{}, false, color);
|
||||
var load_result = cli.loadPortfolioPrices(io, svc, syms, &.{}, ctx.globals.refresh_policy, color);
|
||||
load_result.deinit();
|
||||
}
|
||||
|
||||
|
|
|
|||
59
src/main.zig
59
src/main.zig
|
|
@ -63,12 +63,18 @@ const usage_footer =
|
|||
\\ help / --help Show this message
|
||||
\\
|
||||
\\Global options (must appear before the subcommand):
|
||||
\\ --no-color Disable colored output
|
||||
\\ -p, --portfolio <FILE> Portfolio file (default: portfolio.srf;
|
||||
\\ cwd → ZFIN_HOME). metadata.srf and
|
||||
\\ accounts.srf are loaded from the same
|
||||
\\ directory as the resolved portfolio.
|
||||
\\ -w, --watchlist <FILE> Watchlist file (default: watchlist.srf)
|
||||
\\ --no-color Disable colored output
|
||||
\\ --refresh-data=<value> Cache freshness policy (default: auto):
|
||||
\\ auto respect cache TTLs (default)
|
||||
\\ force re-fetch every symbol regardless
|
||||
\\ of TTL freshness
|
||||
\\ never serve cache contents only;
|
||||
\\ no provider calls (offline mode)
|
||||
\\ -p, --portfolio <FILE> Portfolio file (default: portfolio.srf;
|
||||
\\ cwd → ZFIN_HOME). metadata.srf and
|
||||
\\ accounts.srf are loaded from the same
|
||||
\\ directory as the resolved portfolio.
|
||||
\\ -w, --watchlist <FILE> Watchlist file (default: watchlist.srf)
|
||||
\\
|
||||
\\Interactive command options:
|
||||
\\ -s, --symbol <SYMBOL> Initial symbol (default: VTI)
|
||||
|
|
@ -99,6 +105,10 @@ const Globals = struct {
|
|||
portfolio_path: ?[]const u8 = null,
|
||||
/// Explicit watchlist path from -w/--watchlist (raw, null if not set).
|
||||
watchlist_path: ?[]const u8 = null,
|
||||
/// Cache freshness policy from `--refresh-data=<value>`.
|
||||
/// Default: `.auto` (TTL-respecting). Other values: `.force`
|
||||
/// (re-fetch regardless of TTL) and `.never` (offline mode).
|
||||
refresh_policy: cmd_framework.RefreshPolicy = .auto,
|
||||
/// Index into args of the first post-global token (the subcommand).
|
||||
cursor: usize,
|
||||
};
|
||||
|
|
@ -106,6 +116,8 @@ const Globals = struct {
|
|||
const GlobalParseError = error{
|
||||
MissingValue,
|
||||
UnknownGlobalFlag,
|
||||
/// `--refresh-data=<value>` got something other than auto/force/never.
|
||||
InvalidRefreshDataValue,
|
||||
};
|
||||
|
||||
/// Parse global flags from args[1..] up to the first non-flag (subcommand)
|
||||
|
|
@ -135,6 +147,33 @@ fn parseGlobals(args: []const []const u8) GlobalParseError!Globals {
|
|||
i += 2;
|
||||
continue;
|
||||
}
|
||||
// `--refresh-data=<value>` is a single token: the flag name,
|
||||
// an `=`, and the value (one of auto / force / never). The
|
||||
// single-flag tri-state shape is more honest than the
|
||||
// earlier two-flag (`--refresh` / `--no-refresh`) design
|
||||
// because the user-facing values map 1:1 to the
|
||||
// `RefreshPolicy` enum and impossible-state combinations
|
||||
// are unrepresentable.
|
||||
if (std.mem.startsWith(u8, a, "--refresh-data=")) {
|
||||
const value = a["--refresh-data=".len..];
|
||||
if (std.mem.eql(u8, value, "auto")) {
|
||||
g.refresh_policy = .auto;
|
||||
} else if (std.mem.eql(u8, value, "force")) {
|
||||
g.refresh_policy = .force;
|
||||
} else if (std.mem.eql(u8, value, "never")) {
|
||||
g.refresh_policy = .never;
|
||||
} else {
|
||||
return error.InvalidRefreshDataValue;
|
||||
}
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (std.mem.eql(u8, a, "--refresh-data")) {
|
||||
// Bare `--refresh-data` without `=value` is a user
|
||||
// mistake (probably tried `--refresh-data force` with a
|
||||
// space). Surface the shape mismatch explicitly.
|
||||
return error.MissingValue;
|
||||
}
|
||||
// Help flags are subcommand-like tokens, stop scanning.
|
||||
if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) break;
|
||||
|
||||
|
|
@ -225,6 +264,7 @@ fn runCli(init: std.process.Init) !u8 {
|
|||
}
|
||||
try cli.stderrPrint(io, "\nRun 'zfin help' for usage.\n");
|
||||
},
|
||||
error.InvalidRefreshDataValue => try cli.stderrPrint(io, "Error: --refresh-data=<value> requires one of: auto, force, never.\n"),
|
||||
}
|
||||
return 1;
|
||||
};
|
||||
|
|
@ -325,6 +365,7 @@ fn runCli(init: std.process.Init) !u8 {
|
|||
.no_color = globals.no_color,
|
||||
.portfolio_path = globals.portfolio_path,
|
||||
.watchlist_path = globals.watchlist_path,
|
||||
.refresh_policy = globals.refresh_policy,
|
||||
},
|
||||
.today = today,
|
||||
.now_s = now_s,
|
||||
|
|
@ -357,6 +398,12 @@ fn globalOffender(args: []const []const u8) ?[]const u8 {
|
|||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (std.mem.startsWith(u8, a, "--refresh-data=") or
|
||||
std.mem.eql(u8, a, "--refresh-data"))
|
||||
{
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (std.mem.eql(u8, a, "-p") or std.mem.eql(u8, a, "--portfolio") or
|
||||
std.mem.eql(u8, a, "-w") or std.mem.eql(u8, a, "--watchlist"))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ pub const DataError = error{
|
|||
|
||||
/// Decide whether a provider failure is permanent enough to merit a
|
||||
/// negative-cache entry. Negative entries suppress retries until the
|
||||
/// next manual `--refresh` / `cache clear`, so writing one is only
|
||||
/// next manual `--refresh-data=force` / `cache clear`, so writing one is only
|
||||
/// safe when we're confident more attempts won't succeed.
|
||||
///
|
||||
/// Today the only certain-permanent failure is `NotFound`: the symbol
|
||||
|
|
|
|||
|
|
@ -2248,7 +2248,7 @@ pub fn run(
|
|||
svc,
|
||||
syms,
|
||||
watch_syms.items,
|
||||
false, // force_refresh
|
||||
.auto, // refresh policy: TUI is interactive; honor TTLs
|
||||
true, // color
|
||||
);
|
||||
app_inst.portfolio.prefetched_prices = load_result.prices;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue