---
name: feedback-xlow-maker-position-netting
description: 2026-08-23 — xlow and the maker trade the SAME Kalshi tickers, and the exchange nets them into one position. Three code paths read that netted position as maker fill quantity and silently bricked books. Never infer a maker fill from a position.
metadata:
  type: feedback
---

## The rule

**A position is never evidence of a maker fill.** Kalshi nets every strategy on
this account into ONE position per ticker, so our −3 alongside xlow's +5 reads
+2 — which is neither. The only safe attribution is the maker's own `order_id`,
recorded at post time and matched against `/portfolio/fills`.

## What it did

xlow bought 5 FIO on Roma–Fiorentina while the maker was quoting that book. The
maker read the position change as its own leg selling, with **no price** to
attach, so `collected` counted 5 contracts sold for **$0**. The budget rule then
demanded the whole dollar from the remaining legs and the RATCHET latched the
floors — which only ever rise. RACBOC ended up asking 60c/94c and had to be
cancelled by hand; ROMFIO simply stopped quoting FIO forever.

Symptom to recognise: **absurd asks far above touch, or a leg that never gets
quoted**, on a book where xlow also has a position.

## Three paths, same wrong assumption

All in `main/mm_maker_live.py`. It took three fixes because the assumption was
repeated:

1. **Position fallback** — when no maker fill record matched, it trusted the
   exchange position and invented a fill. Removed; now books nothing and prints
   `[!] <tkr>: short N with no attributable maker fill`.
2. **`held = abs(positions.get(tkr))`** — the real killer. `abs()` turned a LONG
   into sold quantity, re-creating the phantom in the SAME cycle the heal
   cleared it. Now only a short counts, and when any foreign fill exists on the
   ticker, position-based reconciliation is abandoned entirely.
3. **No recovery** — added a self-heal: a booked fill on a leg the exchange
   shows us LONG cannot be ours (this maker only ever sells), so clear
   `fill_qty` AND `floor_px`. Prints `[heal] ...`.

Prevention now: every `order_id` posted is stored per leg in `our_ids` (cap 40),
and attribution requires membership.

## How to check for it

```python
# phantom signature in research/mm_live_state.json
fill_qty > 0 and not fill_ids          # booked a sale with nothing backing it
fill_qty > 0 and fill_px is None       # ... and no price, so it counts as $0
```
Cross-check against the exchange: a POSITIVE position on a leg means it is not
ours. Two were found live (ROMFIO-FIO, FULCFC-FUL).

## ACCEPTED RISK: the two strategies also trade AGAINST each other

The fixes above stop xlow corrupting the maker's ACCOUNTING. They do not stop
the maker quoting a ticker xlow holds, so the two can trade with each other.

Observed 2026-08-24 on Roma-Fiorentina:

```
2026-08-23 18:36  BUY  5 @ 18c  taker   <- xlow
2026-08-24 03:18  SELL 3 @ 18c  maker   <- the market maker
                  NET 2.00 contracts
```

The maker sold 3 of xlow's own contracts at the same price xlow paid. Effects:
- xlow's intended 5-contract bet is really a 2-contract bet
- taker fees paid on 3 contracts that no longer exist
- `pending_bets.json` still records 5 @ 18c, so the FORWARD TEST will score a
  5-contract result on a 2-contract position
- the maker's book budget counts 18c "collected" on a leg it sold to itself

**User decision 2026-08-24: leave it as is.** Not worth the maker coverage to
fix, given xlow places only a few bets a day. Do not re-raise unqueried.

If it is ever revisited, the cheap fix is: before posting a leg, skip it when
the exchange shows a LONG position on that ticker (the maker is only ever
short, so long == xlow is in it). The maker already reads positions each cycle,
so it costs nothing. Would have dropped 1 leg of 15 on the night observed.

## The general lesson

Two strategies on one account is not two isolated systems. Anything derived from
account-level state — positions, balances, order counts, collateral — is shared
and cannot be attributed by inspection. Attribute by identifier or do not
attribute. See [[project-single-machine-consolidation]] for the same lesson at
the machine level, and [[feedback-system-process-invisible]] for it at the
process level.
