add label field, let notes field be strictly for notes
This commit is contained in:
parent
d301466345
commit
cd2ccb4c43
8 changed files with 136 additions and 51 deletions
|
|
@ -127,8 +127,10 @@ pub const PortfolioSummary = struct {
|
||||||
pub const Allocation = struct {
|
pub const Allocation = struct {
|
||||||
/// Ticker symbol or CUSIP identifying this position.
|
/// Ticker symbol or CUSIP identifying this position.
|
||||||
symbol: []const u8,
|
symbol: []const u8,
|
||||||
/// Display label for the symbol column. For CUSIPs with notes, this is a
|
/// Display label for the symbol column — the position's "human
|
||||||
/// short label derived from the note (e.g. "TGT2035"). Otherwise same as symbol.
|
/// identity": an explicit `label::`, else the economic identity
|
||||||
|
/// (`priceSymbol()`). Display-only; never note-derived and never a
|
||||||
|
/// pricing or classification key. See `Position.displaySymbol()`.
|
||||||
display_symbol: []const u8,
|
display_symbol: []const u8,
|
||||||
/// Total shares held across all lots for this symbol.
|
/// Total shares held across all lots for this symbol.
|
||||||
shares: f64,
|
shares: f64,
|
||||||
|
|
@ -392,15 +394,9 @@ pub fn portfolioSummary(
|
||||||
total_cost += pos.total_cost;
|
total_cost += pos.total_cost;
|
||||||
total_realized += pos.realized_gain_loss;
|
total_realized += pos.realized_gain_loss;
|
||||||
|
|
||||||
// For CUSIPs with a note, derive a short display label from the note.
|
|
||||||
const display = if (portfolio_mod.isCusipLike(pos.symbol) and pos.note != null)
|
|
||||||
shortLabel(pos.note.?)
|
|
||||||
else
|
|
||||||
pos.symbol;
|
|
||||||
|
|
||||||
try allocs.append(allocator, .{
|
try allocs.append(allocator, .{
|
||||||
.symbol = pos.symbol,
|
.symbol = pos.symbol,
|
||||||
.display_symbol = display,
|
.display_symbol = pos.displaySymbol(),
|
||||||
.shares = pos.shares,
|
.shares = pos.shares,
|
||||||
.avg_cost = pos.avg_cost,
|
.avg_cost = pos.avg_cost,
|
||||||
.current_price = price,
|
.current_price = price,
|
||||||
|
|
@ -652,49 +648,12 @@ pub fn computeHistoricalSnapshots(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derive a short display label (max 7 chars) from a descriptive note.
|
|
||||||
/// "VANGUARD TARGET 2035" -> "TGT2035", "LARGE COMPANY STOCK" -> "LRG CO".
|
|
||||||
/// Falls back to first 7 characters of the note if no pattern matches.
|
|
||||||
fn shortLabel(note: []const u8) []const u8 {
|
|
||||||
// Look for "TARGET <year>" pattern (Vanguard Target Retirement funds)
|
|
||||||
const target_labels = .{
|
|
||||||
.{ "2025", "TGT2025" },
|
|
||||||
.{ "2030", "TGT2030" },
|
|
||||||
.{ "2035", "TGT2035" },
|
|
||||||
.{ "2040", "TGT2040" },
|
|
||||||
.{ "2045", "TGT2045" },
|
|
||||||
.{ "2050", "TGT2050" },
|
|
||||||
.{ "2055", "TGT2055" },
|
|
||||||
.{ "2060", "TGT2060" },
|
|
||||||
.{ "2065", "TGT2065" },
|
|
||||||
.{ "2070", "TGT2070" },
|
|
||||||
};
|
|
||||||
if (std.ascii.indexOfIgnoreCase(note, "target")) |_| {
|
|
||||||
inline for (target_labels) |entry| {
|
|
||||||
if (std.mem.indexOf(u8, note, entry[0]) != null) {
|
|
||||||
return entry[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fallback: take up to 7 chars from the note
|
|
||||||
const max = @min(note.len, 7);
|
|
||||||
return note[0..max];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Tests ────────────────────────────────────────────────────
|
// ── Tests ────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn makeCandle(date: Date, price: f64) Candle {
|
fn makeCandle(date: Date, price: f64) Candle {
|
||||||
return .{ .date = date, .open = price, .high = price, .low = price, .close = price, .adj_close = price, .volume = 1000 };
|
return .{ .date = date, .open = price, .high = price, .low = price, .close = price, .adj_close = price, .volume = 1000 };
|
||||||
}
|
}
|
||||||
|
|
||||||
test "shortLabel" {
|
|
||||||
try std.testing.expectEqualStrings("TGT2035", shortLabel("VANGUARD TARGET 2035"));
|
|
||||||
try std.testing.expectEqualStrings("TGT2040", shortLabel("VANGUARD TARGET 2040"));
|
|
||||||
try std.testing.expectEqualStrings("LARGE C", shortLabel("LARGE COMPANY STOCK"));
|
|
||||||
try std.testing.expectEqualStrings("SHORT", shortLabel("SHORT"));
|
|
||||||
try std.testing.expectEqualStrings("TGT2055", shortLabel("TARGET 2055 FUND"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "findPriceAtDate exact match" {
|
test "findPriceAtDate exact match" {
|
||||||
const candles = [_]Candle{
|
const candles = [_]Candle{
|
||||||
makeCandle(Date.fromYmd(2024, 1, 2), 100),
|
makeCandle(Date.fromYmd(2024, 1, 2), 100),
|
||||||
|
|
@ -981,6 +940,39 @@ test "portfolioSummary applies price_ratio" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "portfolioSummary: display_symbol uses label, else priceSymbol" {
|
||||||
|
const Position = portfolio_mod.Position;
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
var positions = [_]Position{
|
||||||
|
// Bare CUSIP with an explicit label → the label shows.
|
||||||
|
.{ .symbol = "02315N600", .shares = 100, .avg_cost = 140.0, .total_cost = 14000.0, .open_lots = 1, .closed_lots = 0, .realized_gain_loss = 0, .label = "TGT2035" },
|
||||||
|
// Bare CUSIP without a label → raw CUSIP shows (post-migration default).
|
||||||
|
.{ .symbol = "02315N709", .shares = 10, .avg_cost = 150.0, .total_cost = 1500.0, .open_lots = 1, .closed_lots = 0, .realized_gain_loss = 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
var prices = std.StringHashMap(f64).init(alloc);
|
||||||
|
defer prices.deinit();
|
||||||
|
try prices.put("02315N600", 200.0);
|
||||||
|
try prices.put("02315N709", 100.0);
|
||||||
|
|
||||||
|
const empty_pf = portfolio_mod.Portfolio{ .lots = &.{}, .allocator = alloc };
|
||||||
|
var summary = try portfolioSummary(Date.fromYmd(2026, 5, 8), alloc, empty_pf, &positions, prices, null);
|
||||||
|
defer summary.deinit(alloc);
|
||||||
|
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), summary.allocations.len);
|
||||||
|
for (summary.allocations) |a| {
|
||||||
|
if (std.mem.eql(u8, a.symbol, "02315N600")) {
|
||||||
|
// symbol (the classification key) is unchanged; display shows the label.
|
||||||
|
try std.testing.expectEqualStrings("02315N600", a.symbol);
|
||||||
|
try std.testing.expectEqualStrings("TGT2035", a.display_symbol);
|
||||||
|
} else {
|
||||||
|
// No label → display falls back to the symbol (priceSymbol).
|
||||||
|
try std.testing.expectEqualStrings("02315N709", a.display_symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "portfolioSummary skips price_ratio for manual/fallback prices" {
|
test "portfolioSummary skips price_ratio for manual/fallback prices" {
|
||||||
const Position = portfolio_mod.Position;
|
const Position = portfolio_mod.Position;
|
||||||
const alloc = std.testing.allocator;
|
const alloc = std.testing.allocator;
|
||||||
|
|
|
||||||
36
src/cache/store.zig
vendored
36
src/cache/store.zig
vendored
|
|
@ -1867,6 +1867,7 @@ pub fn deserializePortfolio(allocator: std.mem.Allocator, data: []const u8) !Por
|
||||||
for (lots.items) |lot| {
|
for (lots.items) |lot| {
|
||||||
allocator.free(lot.symbol);
|
allocator.free(lot.symbol);
|
||||||
if (lot.note) |n| allocator.free(n);
|
if (lot.note) |n| allocator.free(n);
|
||||||
|
if (lot.label) |l| allocator.free(l);
|
||||||
if (lot.account) |a| allocator.free(a);
|
if (lot.account) |a| allocator.free(a);
|
||||||
if (lot.ticker) |t| allocator.free(t);
|
if (lot.ticker) |t| allocator.free(t);
|
||||||
if (lot.underlying) |u| allocator.free(u);
|
if (lot.underlying) |u| allocator.free(u);
|
||||||
|
|
@ -1890,6 +1891,7 @@ pub fn deserializePortfolio(allocator: std.mem.Allocator, data: []const u8) !Por
|
||||||
// Dupe owned strings before iterator.deinit() frees the backing buffer
|
// Dupe owned strings before iterator.deinit() frees the backing buffer
|
||||||
lot.symbol = try allocator.dupe(u8, lot.symbol);
|
lot.symbol = try allocator.dupe(u8, lot.symbol);
|
||||||
if (lot.note) |n| lot.note = try allocator.dupe(u8, n);
|
if (lot.note) |n| lot.note = try allocator.dupe(u8, n);
|
||||||
|
if (lot.label) |l| lot.label = try allocator.dupe(u8, l);
|
||||||
if (lot.account) |a| lot.account = try allocator.dupe(u8, a);
|
if (lot.account) |a| lot.account = try allocator.dupe(u8, a);
|
||||||
if (lot.ticker) |t| lot.ticker = try allocator.dupe(u8, t);
|
if (lot.ticker) |t| lot.ticker = try allocator.dupe(u8, t);
|
||||||
if (lot.underlying) |u| lot.underlying = try allocator.dupe(u8, u);
|
if (lot.underlying) |u| lot.underlying = try allocator.dupe(u8, u);
|
||||||
|
|
@ -1903,6 +1905,7 @@ pub fn deserializePortfolio(allocator: std.mem.Allocator, data: []const u8) !Por
|
||||||
else => {
|
else => {
|
||||||
std.log.warn("portfolio: record at line {d} has no symbol, skipping", .{line});
|
std.log.warn("portfolio: record at line {d} has no symbol, skipping", .{line});
|
||||||
if (lot.note) |n| allocator.free(n);
|
if (lot.note) |n| allocator.free(n);
|
||||||
|
if (lot.label) |l| allocator.free(l);
|
||||||
if (lot.account) |a| allocator.free(a);
|
if (lot.account) |a| allocator.free(a);
|
||||||
if (lot.ticker) |t| allocator.free(t);
|
if (lot.ticker) |t| allocator.free(t);
|
||||||
if (lot.underlying) |u| allocator.free(u);
|
if (lot.underlying) |u| allocator.free(u);
|
||||||
|
|
@ -2919,6 +2922,39 @@ test "portfolio: price_ratio round-trip" {
|
||||||
try std.testing.expectApproxEqAbs(@as(f64, 1.0), portfolio2.lots[1].price_ratio, 0.001);
|
try std.testing.expectApproxEqAbs(@as(f64, 1.0), portfolio2.lots[1].price_ratio, 0.001);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "portfolio: label:: round-trip" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
const data =
|
||||||
|
\\#!srfv1
|
||||||
|
\\symbol::02315N600,shares:num:100,open_date::2024-01-15,open_price:num:140.00,note::some annotation,label::TGT2035
|
||||||
|
\\symbol::AAPL,shares:num:10,open_date::2024-03-01,open_price:num:150.00
|
||||||
|
\\
|
||||||
|
;
|
||||||
|
|
||||||
|
var portfolio = try deserializePortfolio(allocator, data);
|
||||||
|
defer portfolio.deinit();
|
||||||
|
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), portfolio.lots.len);
|
||||||
|
// Label parses and is independent of the note.
|
||||||
|
try std.testing.expectEqualStrings("TGT2035", portfolio.lots[0].label.?);
|
||||||
|
try std.testing.expectEqualStrings("some annotation", portfolio.lots[0].note.?);
|
||||||
|
// displaySymbol() prefers the label over the raw CUSIP.
|
||||||
|
try std.testing.expectEqualStrings("TGT2035", portfolio.lots[0].displaySymbol());
|
||||||
|
// No label: displaySymbol() falls back to priceSymbol().
|
||||||
|
try std.testing.expect(portfolio.lots[1].label == null);
|
||||||
|
try std.testing.expectEqualStrings("AAPL", portfolio.lots[1].displaySymbol());
|
||||||
|
|
||||||
|
// Round-trip: the label survives serialize -> deserialize.
|
||||||
|
const reserialized = try serializePortfolio(allocator, portfolio.lots);
|
||||||
|
defer allocator.free(reserialized);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, reserialized, "label::TGT2035") != null);
|
||||||
|
|
||||||
|
var portfolio2 = try deserializePortfolio(allocator, reserialized);
|
||||||
|
defer portfolio2.deinit();
|
||||||
|
try std.testing.expectEqualStrings("TGT2035", portfolio2.lots[0].label.?);
|
||||||
|
try std.testing.expect(portfolio2.lots[1].label == null);
|
||||||
|
}
|
||||||
|
|
||||||
// ── TTL and Negative Cache Tests ─────────────────────────────────
|
// ── TTL and Negative Cache Tests ─────────────────────────────────
|
||||||
|
|
||||||
test "TTL constants are reasonable" {
|
test "TTL constants are reasonable" {
|
||||||
|
|
|
||||||
|
|
@ -812,6 +812,7 @@ fn freeLots(allocator: std.mem.Allocator, lots: []const portfolio_mod.Lot) void
|
||||||
fn freeLot(allocator: std.mem.Allocator, lot: portfolio_mod.Lot) void {
|
fn freeLot(allocator: std.mem.Allocator, lot: portfolio_mod.Lot) void {
|
||||||
allocator.free(lot.symbol);
|
allocator.free(lot.symbol);
|
||||||
if (lot.note) |n| allocator.free(n);
|
if (lot.note) |n| allocator.free(n);
|
||||||
|
if (lot.label) |l| allocator.free(l);
|
||||||
if (lot.account) |a| allocator.free(a);
|
if (lot.account) |a| allocator.free(a);
|
||||||
if (lot.ticker) |t| allocator.free(t);
|
if (lot.ticker) |t| allocator.free(t);
|
||||||
if (lot.underlying) |u| allocator.free(u);
|
if (lot.underlying) |u| allocator.free(u);
|
||||||
|
|
|
||||||
|
|
@ -562,7 +562,7 @@ pub fn display(
|
||||||
for (portfolio.lots) |lot| {
|
for (portfolio.lots) |lot| {
|
||||||
if (lot.security_type != .illiquid) continue;
|
if (lot.security_type != .illiquid) continue;
|
||||||
var il_row_buf: [160]u8 = undefined;
|
var il_row_buf: [160]u8 = undefined;
|
||||||
try out.print("{s}\n", .{fmt.fmtIlliquidRow(&il_row_buf, lot.symbol, lot.shares, lot.note)});
|
try out.print("{s}\n", .{fmt.fmtIlliquidRow(&il_row_buf, lot.displaySymbol(), lot.shares, lot.note)});
|
||||||
}
|
}
|
||||||
// Illiquid total
|
// Illiquid total
|
||||||
var il_sep_buf2: [80]u8 = undefined;
|
var il_sep_buf2: [80]u8 = undefined;
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,12 @@ pub const Lot = struct {
|
||||||
close_price: ?f64 = null,
|
close_price: ?f64 = null,
|
||||||
/// Optional note/tag for the lot
|
/// Optional note/tag for the lot
|
||||||
note: ?[]const u8 = null,
|
note: ?[]const u8 = null,
|
||||||
|
/// Optional explicit display label — the lot's "human identity"
|
||||||
|
/// for the symbol column (e.g. `label::TGT2035` on a target-date
|
||||||
|
/// CUSIP). When set it overrides the symbol/ticker in display
|
||||||
|
/// ONLY; it is never a pricing or classification key. The display
|
||||||
|
/// counterpart to `ticker::`/`priceSymbol()`. See `displaySymbol()`.
|
||||||
|
label: ?[]const u8 = null,
|
||||||
/// Optional account identifier (e.g. "Roth IRA", "Brokerage")
|
/// Optional account identifier (e.g. "Roth IRA", "Brokerage")
|
||||||
account: ?[]const u8 = null,
|
account: ?[]const u8 = null,
|
||||||
/// Type of holding (stock, option, cd, cash)
|
/// Type of holding (stock, option, cd, cash)
|
||||||
|
|
@ -209,11 +215,23 @@ pub const Lot = struct {
|
||||||
/// Call or put (for option lots).
|
/// Call or put (for option lots).
|
||||||
option_type: OptionType = .call,
|
option_type: OptionType = .call,
|
||||||
|
|
||||||
/// The symbol to use for price fetching (ticker if set, else symbol).
|
/// The symbol to use for price fetching: the `ticker::` alias
|
||||||
|
/// when set, else the raw `symbol`. This is the lot's **economic
|
||||||
|
/// identity** — what the pipeline prices, aggregates, and
|
||||||
|
/// classifies by. Its display counterpart is `displaySymbol()`.
|
||||||
pub fn priceSymbol(self: Lot) []const u8 {
|
pub fn priceSymbol(self: Lot) []const u8 {
|
||||||
return self.ticker orelse self.symbol;
|
return self.ticker orelse self.symbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The symbol to show in the display: an explicit `label::` when
|
||||||
|
/// set, else the economic identity (`priceSymbol()`). This is the
|
||||||
|
/// lot's **human identity** — purely cosmetic, never a pricing or
|
||||||
|
/// classification key. The display mirror of `priceSymbol()`;
|
||||||
|
/// also mirrored by `Position.displaySymbol()`.
|
||||||
|
pub fn displaySymbol(self: Lot) []const u8 {
|
||||||
|
return self.label orelse self.priceSymbol();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn isOpen(self: Lot, as_of: Date) bool {
|
pub fn isOpen(self: Lot, as_of: Date) bool {
|
||||||
return self.lotIsOpenAsOf(as_of);
|
return self.lotIsOpenAsOf(as_of);
|
||||||
}
|
}
|
||||||
|
|
@ -301,6 +319,9 @@ pub const Position = struct {
|
||||||
account: []const u8 = "",
|
account: []const u8 = "",
|
||||||
/// Note from the first lot (e.g. "VANGUARD TARGET 2035").
|
/// Note from the first lot (e.g. "VANGUARD TARGET 2035").
|
||||||
note: ?[]const u8 = null,
|
note: ?[]const u8 = null,
|
||||||
|
/// Explicit display label from the first lot (the lot's `label::`).
|
||||||
|
/// Drives `displaySymbol()`; display-only, never a key.
|
||||||
|
label: ?[]const u8 = null,
|
||||||
/// Price ratio for institutional share classes (from lot).
|
/// Price ratio for institutional share classes (from lot).
|
||||||
/// positionsAsOf() groups by (priceSymbol, price_ratio), so lots with
|
/// positionsAsOf() groups by (priceSymbol, price_ratio), so lots with
|
||||||
/// different ratios sharing the same ticker produce separate positions.
|
/// different ratios sharing the same ticker produce separate positions.
|
||||||
|
|
@ -319,6 +340,15 @@ pub const Position = struct {
|
||||||
pub fn marketValue(self: Position, raw_price: f64, is_preadjusted: bool) f64 {
|
pub fn marketValue(self: Position, raw_price: f64, is_preadjusted: bool) f64 {
|
||||||
return self.shares * self.effectivePrice(raw_price, is_preadjusted);
|
return self.shares * self.effectivePrice(raw_price, is_preadjusted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The symbol to show in the display: an explicit `label` (from
|
||||||
|
/// the lot's `label::`) when set, else `symbol` — which is
|
||||||
|
/// already the economic identity (`priceSymbol()`), since
|
||||||
|
/// positions are keyed by it. The aggregate mirror of
|
||||||
|
/// `Lot.displaySymbol()`.
|
||||||
|
pub fn displaySymbol(self: Position) []const u8 {
|
||||||
|
return self.label orelse self.symbol;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A portfolio is a collection of lots.
|
/// A portfolio is a collection of lots.
|
||||||
|
|
@ -330,6 +360,7 @@ pub const Portfolio = struct {
|
||||||
for (self.lots) |lot| {
|
for (self.lots) |lot| {
|
||||||
self.allocator.free(lot.symbol);
|
self.allocator.free(lot.symbol);
|
||||||
if (lot.note) |n| self.allocator.free(n);
|
if (lot.note) |n| self.allocator.free(n);
|
||||||
|
if (lot.label) |l| self.allocator.free(l);
|
||||||
if (lot.account) |a| self.allocator.free(a);
|
if (lot.account) |a| self.allocator.free(a);
|
||||||
if (lot.ticker) |t| self.allocator.free(t);
|
if (lot.ticker) |t| self.allocator.free(t);
|
||||||
if (lot.underlying) |u| self.allocator.free(u);
|
if (lot.underlying) |u| self.allocator.free(u);
|
||||||
|
|
@ -456,6 +487,7 @@ pub const Portfolio = struct {
|
||||||
.realized_gain_loss = 0,
|
.realized_gain_loss = 0,
|
||||||
.account = lot.account orelse "",
|
.account = lot.account orelse "",
|
||||||
.note = lot.note,
|
.note = lot.note,
|
||||||
|
.label = lot.label,
|
||||||
.price_ratio = lot.price_ratio,
|
.price_ratio = lot.price_ratio,
|
||||||
});
|
});
|
||||||
found = &result.items[result.items.len - 1];
|
found = &result.items[result.items.len - 1];
|
||||||
|
|
@ -527,6 +559,7 @@ pub const Portfolio = struct {
|
||||||
.realized_gain_loss = 0,
|
.realized_gain_loss = 0,
|
||||||
.account = lot_acct,
|
.account = lot_acct,
|
||||||
.note = lot.note,
|
.note = lot.note,
|
||||||
|
.label = lot.label,
|
||||||
.price_ratio = lot.price_ratio,
|
.price_ratio = lot.price_ratio,
|
||||||
});
|
});
|
||||||
found = &result.items[result.items.len - 1];
|
found = &result.items[result.items.len - 1];
|
||||||
|
|
@ -834,6 +867,28 @@ test "Lot.priceSymbol" {
|
||||||
try std.testing.expectEqualStrings("AAPL", without_ticker.priceSymbol());
|
try std.testing.expectEqualStrings("AAPL", without_ticker.priceSymbol());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Lot.displaySymbol: label orelse priceSymbol" {
|
||||||
|
const base = Lot{ .symbol = "02315N600", .shares = 1, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 100 };
|
||||||
|
// No label, no ticker: falls back to the raw symbol (the CUSIP).
|
||||||
|
try std.testing.expectEqualStrings("02315N600", base.displaySymbol());
|
||||||
|
// No label, ticker set: falls back to priceSymbol (the ticker).
|
||||||
|
var aliased = base;
|
||||||
|
aliased.ticker = "VTTHX";
|
||||||
|
try std.testing.expectEqualStrings("VTTHX", aliased.displaySymbol());
|
||||||
|
// Explicit label wins over both symbol and ticker.
|
||||||
|
var labeled = aliased;
|
||||||
|
labeled.label = "TGT2035";
|
||||||
|
try std.testing.expectEqualStrings("TGT2035", labeled.displaySymbol());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Position.displaySymbol: label orelse symbol" {
|
||||||
|
// Position.symbol is already priceSymbol(), so symbol is the fallback.
|
||||||
|
const no_label = Position{ .symbol = "VTTHX", .shares = 1, .avg_cost = 0, .total_cost = 0, .open_lots = 1, .closed_lots = 0, .realized_gain_loss = 0 };
|
||||||
|
try std.testing.expectEqualStrings("VTTHX", no_label.displaySymbol());
|
||||||
|
const labeled = Position{ .symbol = "VTTHX", .shares = 1, .avg_cost = 0, .total_cost = 0, .open_lots = 1, .closed_lots = 0, .realized_gain_loss = 0, .label = "TGT2035" };
|
||||||
|
try std.testing.expectEqualStrings("TGT2035", labeled.displaySymbol());
|
||||||
|
}
|
||||||
|
|
||||||
test "Lot.returnPct" {
|
test "Lot.returnPct" {
|
||||||
// Open lot: uses current_price param
|
// Open lot: uses current_price param
|
||||||
const open_lot = Lot{ .symbol = "AAPL", .shares = 10, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 100 };
|
const open_lot = Lot{ .symbol = "AAPL", .shares = 10, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 100 };
|
||||||
|
|
|
||||||
|
|
@ -393,6 +393,7 @@ fn loadFromBytes(
|
||||||
for (merged.items) |lot| {
|
for (merged.items) |lot| {
|
||||||
allocator.free(lot.symbol);
|
allocator.free(lot.symbol);
|
||||||
if (lot.note) |n| allocator.free(n);
|
if (lot.note) |n| allocator.free(n);
|
||||||
|
if (lot.label) |l| allocator.free(l);
|
||||||
if (lot.account) |a| allocator.free(a);
|
if (lot.account) |a| allocator.free(a);
|
||||||
if (lot.ticker) |t| allocator.free(t);
|
if (lot.ticker) |t| allocator.free(t);
|
||||||
if (lot.underlying) |u| allocator.free(u);
|
if (lot.underlying) |u| allocator.free(u);
|
||||||
|
|
|
||||||
|
|
@ -1726,7 +1726,7 @@ pub fn drawContent(state: *State, app: *App, arena: std.mem.Allocator, buf: []va
|
||||||
.illiquid_row => {
|
.illiquid_row => {
|
||||||
if (row.lot) |lot| {
|
if (row.lot) |lot| {
|
||||||
var illiquid_row_buf: [160]u8 = undefined;
|
var illiquid_row_buf: [160]u8 = undefined;
|
||||||
const row_text = fmt.fmtIlliquidRow(&illiquid_row_buf, row.symbol, lot.shares, lot.note);
|
const row_text = fmt.fmtIlliquidRow(&illiquid_row_buf, lot.displaySymbol(), lot.shares, lot.note);
|
||||||
const text = try std.fmt.allocPrint(arena, " {s}", .{row_text});
|
const text = try std.fmt.allocPrint(arena, " {s}", .{row_text});
|
||||||
const row_style7 = if (is_cursor) th.selectStyle() else th.mutedStyle();
|
const row_style7 = if (is_cursor) th.selectStyle() else th.mutedStyle();
|
||||||
try lines.append(arena, .{ .text = text, .style = row_style7 });
|
try lines.append(arena, .{ .text = text, .style = row_style7 });
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ pub const Options = struct {
|
||||||
const acct = lot.account orelse "";
|
const acct = lot.account orelse "";
|
||||||
|
|
||||||
const text = try std.fmt.allocPrint(allocator, OptionsLayout.data_row, .{
|
const text = try std.fmt.allocPrint(allocator, OptionsLayout.data_row, .{
|
||||||
lot.symbol,
|
lot.displaySymbol(),
|
||||||
qty,
|
qty,
|
||||||
std.fmt.bufPrint(&cost_buf, "{f}", .{Money.from(cost_per)}) catch "$?",
|
std.fmt.bufPrint(&cost_buf, "{f}", .{Money.from(cost_per)}) catch "$?",
|
||||||
prem_str,
|
prem_str,
|
||||||
|
|
@ -244,7 +244,7 @@ pub const CDs = struct {
|
||||||
const acct = lot.account orelse "";
|
const acct = lot.account orelse "";
|
||||||
|
|
||||||
const text = try std.fmt.allocPrint(allocator, CDsLayout.data_row, .{
|
const text = try std.fmt.allocPrint(allocator, CDsLayout.data_row, .{
|
||||||
lot.symbol,
|
lot.displaySymbol(),
|
||||||
std.fmt.bufPrint(&face_buf, "{f}", .{Money.from(lot.shares)}) catch "$?",
|
std.fmt.bufPrint(&face_buf, "{f}", .{Money.from(lot.shares)}) catch "$?",
|
||||||
rate_str,
|
rate_str,
|
||||||
mat_str,
|
mat_str,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue