Files
Jeroen Grusewski 6783932860 fix(exec): stop the UCITS wash trade — one SSOT for ticker vs broker symbol space
Live 2026-07-21/22 the UCITS rebalancer sold and re-bought the SAME 108 shares
every run: $0 position change, ~$23/day in fees+spread (~$5.9k/yr on a $100k
envelope), and a churning pattern that would be a compliance problem live.

Root cause — TWO symbol spaces were compared directly. IBKR reports the IEF line
under its canonical symbol CSBGU0; our weights/prices/targets key on the ticker
CBU0. Two independent sites got this wrong and together formed the loop:

1. execution.py:_plan — `current = acct.positions.get(sym)` read a ticker key out
   of an IBKR-keyed dict, so it saw 0 held while 108 shares existed. delta became
   the FULL target instead of ~0 -> re-BUY every run, and the weight-0 exit path
   could never fire. (The more severe half: it mis-sizes every UCITS rebalance,
   not just this line.)
2. multistrat_exec_record.superseded_holdings — `sym not in current_targets`
   classified the legitimate CSBGU0 holding as an orphan -> SELL every run.

Fix: a single translation SSOT on UcitsSettings (config.py), next to the map that
already owns both symbols — ibkr_symbol_for / ticker_for / to_ticker_space. Both
sites now translate broker-reported data into ticker space before any comparison,
mirroring the pattern dashboard_service._expected_book_symbols already used for
the display-only stray check (which was correct, and is why this stayed hidden).

Regression tests encode the live case and were verified to FAIL on the old code:
- test_ucits_holding_reported_under_broker_symbol_is_recognised_as_on_target
  (unchanged holding reported as CSBGU0 vs CBU0 target -> ZERO orders)
- test_superseded_does_not_flag_the_ibkr_reported_name_of_a_current_target
- test_superseded_still_flags_a_genuinely_retired_line_alongside_the_remapped_one
  (the self-clean must keep working — IDTM is still liquidated)
- test_ucits_symbol_space_helpers_round_trip

65 tests green (execution, exec helpers, b2a, exec_capture, reconcile, cli).
No new lint (17 ruff findings before and after — all pre-existing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:58:56 +00:00
..