diff --git a/src/commands/analysis.zig b/src/commands/analysis.zig index 5b60a4b..0433900 100644 --- a/src/commands/analysis.zig +++ b/src/commands/analysis.zig @@ -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| { diff --git a/src/commands/audit.zig b/src/commands/audit.zig index e7eadf6..176a1f4 100644 --- a/src/commands/audit.zig +++ b/src/commands/audit.zig @@ -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| { diff --git a/src/commands/common.zig b/src/commands/common.zig index 17a78c7..8f1fbd8 100644 --- a/src/commands/common.zig +++ b/src/commands/common.zig @@ -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(), ); diff --git a/src/commands/compare.zig b/src/commands/compare.zig index a6dae68..7c9cf0b 100644 --- a/src/commands/compare.zig +++ b/src/commands/compare.zig @@ -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 {}; diff --git a/src/commands/contributions.zig b/src/commands/contributions.zig index 22da23e..a789450 100644 --- a/src/commands/contributions.zig +++ b/src/commands/contributions.zig @@ -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; }; diff --git a/src/commands/framework.zig b/src/commands/framework.zig index d31b103..b80b084 100644 --- a/src/commands/framework.zig +++ b/src/commands/framework.zig @@ -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=` 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=`. + 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" { diff --git a/src/commands/portfolio.zig b/src/commands/portfolio.zig index ae812e7..57abff2 100644 --- a/src/commands/portfolio.zig +++ b/src/commands/portfolio.zig @@ -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=` + \\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" { diff --git a/src/commands/snapshot.zig b/src/commands/snapshot.zig index 0326afb..273da15 100644 --- a/src/commands/snapshot.zig +++ b/src/commands/snapshot.zig @@ -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(); } diff --git a/src/main.zig b/src/main.zig index 298fea6..8a11a15 100644 --- a/src/main.zig +++ b/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 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 Watchlist file (default: watchlist.srf) + \\ --no-color Disable colored output + \\ --refresh-data= 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 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 Watchlist file (default: watchlist.srf) \\ \\Interactive command options: \\ -s, --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=`. + /// 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=` 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=` 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= 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")) { diff --git a/src/service.zig b/src/service.zig index e1da597..64c886f 100644 --- a/src/service.zig +++ b/src/service.zig @@ -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 diff --git a/src/tui.zig b/src/tui.zig index 101b153..b617b38 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -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;