zfin/src/models/snapshot.zig

106 lines
3.8 KiB
Zig

//! Snapshot record types — the wire format for `history/<date>-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");
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);
}
};