106 lines
3.8 KiB
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);
|
|
}
|
|
};
|