[BUG] Broker sync maps MERGER, reverse SPLIT, and forward SPLIT corporate actions to `UNKNOWN`

Author: j-klierCreated Sep 17, 2026Updated Sep 19, 2026

map_broker_activity() takes an activity's activity_type verbatim from the broker feed and never inspects description to recover a better classification. As a result, three common, recurring corporate-action shapes are left permanently UNKNOWN, even though description follows a fixed, unambiguous wording pattern:

  1. Mergers/reorganizations — description ends "RESULT OF MERGER" (raw broker type XFER). Should map to TRANSFER_OUT (departing security) / TRANSFER_IN (arriving security).
  2. Reverse splits — description ends "RESULT OF REVERSE SPLIT" (raw broker type XFER). Should map to SPLIT.
  3. Forward stock splits — description matches "STOCK SPLIT ON N SHARES AT X PER SHARE" (raw broker type CASH). Should map to SPLIT, with X printed directly in the description as the real ratio.

Reclassifying activity_type alone is not enough for reverse splits, and doing it naively is actively dangerous — this is the compounding issue that makes the reverse-split case harder than the other two.

Root Cause

map_broker_activity() derived activity_type exclusively from the broker's own activity_type field:

rust
let raw_activity_type = activity
    .activity_type
    .clone()
    .map(|t| t.trim().to_uppercase())
    .filter(|t| !t.is_empty())
    .unwrap_or_else(|| "UNKNOWN".to_string());

There is no branch anywhere in this function, or upstream of it, that inspected description for these patterns. Whatever upstream step first assigns activity_type = "UNKNOWN" for a generic XFER/CASH raw type it can't otherwise interpret has no visibility into description either — it had already lost the classification opportunity by the time this function ran.

The reverse-split "double SPLIT" risk

A reverse split arrives as two separate broker activities — a departing-share ("FROM") leg and an arriving-share ("TO") leg, mirroring the two lines a brokerage statement prints for this event. Wealthfolio's own apply_split() (crates/core/src/portfolio/snapshot/positions_model.rs) is a single-row, non-idempotent operation:

rust
lot.split_ratio = prior * split_ratio;

applied once per SPLIT-typed activity, unconditionally, with no matching/pairing logic at all. If both legs of a reverse split were naively mapped to SPLIT carrying the same real, nonzero ratio, the ratio would be applied twice, compounding e.g. a real 1-for-2 reverse split into an effective 1-for-4 — a silent, real position-corrupting bug, not merely a cosmetic one.

The correct fix must ensure only the TO leg ever carries the real ratio; the FROM leg's own amount must be a deliberate no-op (1.0lot.split_ratio = prior * 1 = prior, unchanged), which is also the only value that both (a) passes Wealthfolio's own has_valid_split_ratio() validation (activity_type == SPLIT requires a strictly positive amount; the broker's own raw amount for these rows is 0.0, which fails outright) and (b) is harmless even in the worst case where it's somehow still applied.

Description drafted by Claude Sonnet 5