move snapshot types into a new model for reuse

This commit is contained in:
Emil Lerch 2026-04-22 03:54:33 -07:00
parent ea0dd624b3
commit a7e8dc6030
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 104 additions and 74 deletions

View file

@ -22,6 +22,17 @@ const atomic = @import("../atomic.zig");
const version = @import("../version.zig");
const portfolio_mod = @import("../models/portfolio.zig");
const Date = @import("../models/date.zig").Date;
const model = @import("../models/snapshot.zig");
// Re-export record types so callers that reach `commands/snapshot.zig`
// (tests, mostly) still see the familiar names. New code should prefer
// `@import("models/snapshot.zig")` directly.
pub const MetaRow = model.MetaRow;
pub const TotalRow = model.TotalRow;
pub const TaxTypeRow = model.TaxTypeRow;
pub const AccountRow = model.AccountRow;
pub const LotRow = model.LotRow;
pub const Snapshot = model.Snapshot;
pub const SnapshotError = error{
PortfolioEmpty,
@ -356,80 +367,10 @@ pub fn quoteDateRange(infos: []const QuoteInfo) ?struct { min: Date, max: Date }
// Snapshot records
//
// Each record kind below is a plain struct suitable for `srf.fmtFrom`.
// Field order in the struct declaration IS the on-disk order srf's
// `Record.from` iterates `inline for (info.fields)`. The leading `kind`
// field is the discriminator readers demux on.
//
// IMPORTANT: `kind` does NOT have a default value. SRF elides fields
// whose value matches the declared default (see setField in srf.zig),
// so a `kind: []const u8 = "meta"` would vanish from the output. Each
// construction site supplies the tag explicitly.
//
// Optional fields default to `null` so they're elided on null values
// that's the behavior we want for `price`, `quote_date`, etc.
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,
};
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);
}
};
// Record structs live in `src/models/snapshot.zig` see the re-exports
// near the top of this file. The types are separated from this command
// module so analytics code (`src/analytics/timeline.zig`) can reference
// them without depending on a `commands/` module.
/// Build the full snapshot in memory. Does not touch disk.
fn buildSnapshot(

89
src/models/snapshot.zig Normal file
View file

@ -0,0 +1,89 @@
//! 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.parse` `Record.to(T)` on the read side.
//! Field order in the struct declaration IS the on-disk order: srf's
//! `Record.from` iterates `inline for (info.fields)`. The leading `kind`
//! field is the discriminator readers use to demux heterogeneous records.
//!
//! 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` does NOT have a default value. SRF elides fields
//! whose value matches the declared default (see `setField` in
//! `srf.zig`), so `kind: []const u8 = "meta"` would vanish from the
//! output. Each construction site supplies the tag explicitly.
//!
//! 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.
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);
}
};