//! Snapshot record types — the wire format for `history/-portfolio.srf`. //! //! Each record kind below is a plain struct suitable for `srf.fmtFrom` //! on the write side and `srf.iterator` + `FieldIterator.to(Union)` on //! the read side (see `src/history.zig`, which demuxes via a tagged //! `SnapshotRecord` union whose `srf_tag_field = "kind"`). Field order //! in the struct declaration IS the on-disk order: srf's `Record.from` //! iterates `inline for (info.fields)`. //! //! Lives in `src/models/` because these types describe the data format //! itself — they're consumed by the snapshot writer command //! (`src/commands/snapshot.zig`), the history reader (`src/history.zig`), //! and the timeline analytics (`src/analytics/timeline.zig`). Putting //! them in `commands/` would force analytics to depend on a command //! module, which would be backwards. //! //! IMPORTANT: `kind` uses `= ""` as the default — a sentinel that never //! matches any real discriminator value. This satisfies two constraints //! simultaneously: //! - On write, srf elides fields whose value matches the default. Since //! real tags are "meta"/"total"/... (never `""`), `kind` is always //! emitted. //! - On read via the `SnapshotRecord` tagged union (see `history.zig`), //! srf's union dispatch consumes the `kind` field itself before //! coercing into a variant struct, so `kind` is absent from the //! variant's field stream. The default value prevents a //! `FieldNotFoundOnFieldWithoutDefaultValue` error. The post-parse //! `kind` field on variant rows is `""` and should not be consulted — //! the union tag carries the discriminator. //! //! Optional fields default to `null` so they're elided on null values — //! that's the behavior we want for `price`, `quote_date`, etc. const std = @import("std"); const Date = @import("date.zig").Date; pub const MetaRow = struct { kind: []const u8 = "", snapshot_version: u32, as_of_date: Date, captured_at: i64, zfin_version: []const u8, quote_date_min: ?Date = null, quote_date_max: ?Date = null, stale_count: usize, }; pub const TotalRow = struct { kind: []const u8 = "", scope: []const u8, value: f64, }; pub const TaxTypeRow = struct { kind: []const u8 = "", label: []const u8, value: f64, }; pub const AccountRow = struct { kind: []const u8 = "", name: []const u8, value: f64, }; pub const LotRow = struct { kind: []const u8 = "", symbol: []const u8, lot_symbol: []const u8, account: []const u8, security_type: []const u8, shares: f64, open_price: f64, cost_basis: f64, value: f64, /// Null for non-stock lots (cash/CD/illiquid have no per-share price). price: ?f64 = null, /// Null for non-stock lots. quote_date: ?Date = null, /// Emitted only when true (default is false, which srf skips). quote_stale: bool = false, }; /// In-memory portfolio snapshot. `meta` is a single record; the four /// slices are per-section record collections. /// /// When constructed by `history.parseSnapshotBytes`, all string fields /// inside the rows (`symbol`, `label`, etc.) are slices into the /// caller-owned `bytes` buffer, NOT independently-allocated copies. /// The caller is responsible for keeping that buffer alive at least as /// long as the `Snapshot` — typical pattern is `defer allocator.free(bytes)` /// placed AFTER the `defer snap.deinit(allocator)`. pub const Snapshot = struct { meta: MetaRow, totals: []TotalRow, tax_types: []TaxTypeRow, accounts: []AccountRow, lots: []LotRow, pub fn deinit(self: *Snapshot, allocator: std.mem.Allocator) void { allocator.free(self.totals); allocator.free(self.tax_types); allocator.free(self.accounts); allocator.free(self.lots); } };