match only on the calculated symbol, never on display (which could pick up notes)
This commit is contained in:
parent
be888069c0
commit
d301466345
1 changed files with 65 additions and 5 deletions
|
|
@ -605,13 +605,21 @@ pub fn analyzePortfolio(
|
|||
const mv = alloc.market_value;
|
||||
if (mv <= 0) continue;
|
||||
|
||||
// Find classification entries for this symbol
|
||||
// Try both the raw symbol and display_symbol
|
||||
// Find classification entries for this symbol.
|
||||
//
|
||||
// Match on `alloc.symbol` only — the canonical economic
|
||||
// identity (priceSymbol(): the `ticker::` alias when set,
|
||||
// else the raw symbol/CUSIP). `display_symbol` is a
|
||||
// display-only concern (an explicit `label::`, else
|
||||
// priceSymbol) and must NEVER be a classification key:
|
||||
// a free-text annotation that silently changed what
|
||||
// classifies would be a footgun. Keying on `alloc.symbol`
|
||||
// keeps this engine, review's `bucketForSymbol`, and
|
||||
// doctor's `classifiableSymbols` all matching on
|
||||
// priceSymbol().
|
||||
var found = false;
|
||||
for (classifications.entries) |entry| {
|
||||
if (std.mem.eql(u8, entry.symbol, alloc.symbol) or
|
||||
std.mem.eql(u8, entry.symbol, alloc.display_symbol))
|
||||
{
|
||||
if (std.mem.eql(u8, entry.symbol, alloc.symbol)) {
|
||||
found = true;
|
||||
const frac = entry.pct / 100.0;
|
||||
const portion = mv * frac;
|
||||
|
|
@ -1848,6 +1856,58 @@ test "analyzePortfolio: GICS-sectored stock lands in Equity bucket" {
|
|||
try std.testing.expectApproxEqAbs(@as(f64, 50_000), result.asset_category[0].value, 1.0);
|
||||
}
|
||||
|
||||
test "analyzePortfolio: display_symbol is never a classification key" {
|
||||
// Regression: a note/label-derived `display_symbol` must not
|
||||
// classify. The engine keys on `alloc.symbol` (priceSymbol())
|
||||
// only, so a metadata entry written against the display label
|
||||
// classifies nothing — editing a label or note can't move a
|
||||
// single breakdown dollar.
|
||||
const allocator = std.testing.allocator;
|
||||
const allocations = [_]Allocation{
|
||||
.{
|
||||
.symbol = "02315N600", // bare CUSIP: the economic identity
|
||||
.display_symbol = "TGT2035", // human label: the would-be footgun
|
||||
.shares = 1,
|
||||
.avg_cost = 50_000,
|
||||
.current_price = 50_000,
|
||||
.market_value = 50_000,
|
||||
.cost_basis = 50_000,
|
||||
.weight = 1.0,
|
||||
.unrealized_gain_loss = 0.0,
|
||||
.unrealized_return = 0.0,
|
||||
},
|
||||
};
|
||||
const portfolio = Portfolio{ .lots = &.{}, .allocator = allocator };
|
||||
|
||||
// Metadata keyed on the display label must NOT classify; the
|
||||
// holding falls through to unclassified (where display_symbol is
|
||||
// still the friendly label shown to the user).
|
||||
{
|
||||
var entries = [_]ClassificationEntry{
|
||||
.{ .symbol = "TGT2035", .sector = "Technology" },
|
||||
};
|
||||
const cm = ClassificationMap{ .entries = &entries, .allocator = allocator };
|
||||
var result = try analyzePortfolio(allocator, &allocations, cm, portfolio, 50_000, null, Date.fromYmd(2024, 6, 1));
|
||||
defer result.deinit(allocator);
|
||||
try std.testing.expectEqual(@as(usize, 0), result.asset_category.len);
|
||||
try std.testing.expectEqual(@as(usize, 1), result.unclassified.len);
|
||||
try std.testing.expectEqualStrings("TGT2035", result.unclassified[0]);
|
||||
}
|
||||
|
||||
// Metadata keyed on the CUSIP (the economic identity) DOES classify.
|
||||
{
|
||||
var entries = [_]ClassificationEntry{
|
||||
.{ .symbol = "02315N600", .sector = "Technology" },
|
||||
};
|
||||
const cm = ClassificationMap{ .entries = &entries, .allocator = allocator };
|
||||
var result = try analyzePortfolio(allocator, &allocations, cm, portfolio, 50_000, null, Date.fromYmd(2024, 6, 1));
|
||||
defer result.deinit(allocator);
|
||||
try std.testing.expectEqual(@as(usize, 0), result.unclassified.len);
|
||||
try std.testing.expectEqual(@as(usize, 1), result.asset_category.len);
|
||||
try std.testing.expectEqualStrings(bucket_equity, result.asset_category[0].label);
|
||||
}
|
||||
}
|
||||
|
||||
test "analyzePortfolio: empty portfolio produces empty asset_category" {
|
||||
const allocator = std.testing.allocator;
|
||||
const cm = ClassificationMap{ .entries = &.{}, .allocator = allocator };
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue