Closing Binance Positions, Phantom Shorts, and Budget Drift
Context
I’m building Egis, a private trading system (Laravel 12 + Inertia + React) with multiple exchanges:
- Binance Futures (testnet)
- Paper Exchange (simulated)
The system has a shared internal positions table used for:
- UI (Bot Detail → Positions)
- Budget calculation
- Risk checks
Each exchange may also have exchange-specific tables (e.g. paper_positions).
1. The Initial Symptom
After closing a LONG position on Binance:
- ✅ The real Binance position closed correctly
- ✅ The Egis position was marked
closed - ❌ A new SHORT position appeared in Egis
This SHORT was not real. It existed only in Egis.
2. Root Cause: Close Orders Treated as Open Orders
On Binance Futures:
- Closing a LONG = MARKET SELL
- Opening a SHORT = MARKET SELL
Egis had a shared code path that interpreted:
“Any filled SELL order → open a SHORT position”
This logic is valid for open intent, but invalid for close intent.
Key missing concept
There was no explicit distinction between:
- Order intent = OPEN
- Order intent = CLOSE
So the close order was:
- Correctly closing the LONG
- Then mistakenly processed again as an OPEN SELL
- Resulting in a phantom SHORT
3. Fix: Make Order Intent Explicit
The architectural rule introduced:
- Internal orders must have
intent(orrole):openclose
- Close orders must reference the position being closed
- Only open orders may create positions
- Close orders may only update existing positions
This guard permanently prevents:
- Close → open inversion
- Phantom positions from shared handlers
4. Cleaning Up Existing Bad Data
A one-time Artisan command was created:
egis:cleanup-invalid-short-positions
Why this was tricky
The bad SHORT positions:
- Could have
position_amt = 0or > 0 - Were created at the same second as the LONG close
- Did not match the LONG’s entry price
- Did match the LONG’s exit/close price
Final reliable detection rule
A SHORT is erroneous if:
exchange = binanceside = shortstatus = open- There exists a CLOSED LONG with:
- same bot
- same symbol
abs(short.created_at − long.closed_at) < 3 secondsshort.entry_price == long.exit_priceshort.size_usd == long.size_usd
The command supports:
--debug(full reasoning output)- dry-run by default
--confirmto actually mutate data
5. Secondary Bug: Negative Budget With No Open Positions
After fixing the phantom SHORT, a new issue appeared:
“Not enough budget. Available: $-0.01, Requested: $10.00.”
Even though no positions were open.
Investigation result
The issue was not precision.
It was data drift.
6. Paper Exchange Drift: Two Tables, Two Truths
For the Paper Exchange, Egis had:
paper_positions(exchange-specific)positions(internal, used for budget + UI)
They were not always updated together.
As a result:
- Budget logic read from
positions - Actual state lived in
paper_positions - Closed or deleted trades could still “consume” capital
This caused:
- Phantom allocated budget
- Negative available balance
- False “Not enough budget” errors
7. Architectural Decision: Single Source of Truth
Final rule:
Internal
positionstable is the source of truth.
- Budget, UI, and risk logic use only
positions - Exchange tables (
paper_positions) are treated as raw exchange state - Every open/close must update:
- internal positions
- exchange positions
in the same DB transaction
8. Safety Net: Reconciliation Command
A reconciliation command was added/planned:
egis:reconcile-paper-positions
Capabilities:
- Detect mismatches between
positionsandpaper_positions - Dry-run by default
- Optional
--fix/--confirm - DB-only, no external calls
This ensures future drift is:
- Detectable
- Repairable
- Auditable
9. Lessons Learned
1. Close ≠ Open (even if the API call looks identical)
Intent must be explicit in trading systems.
2. Shared handlers are dangerous without intent guards
If “SELL” can mean two things, code must know which.
3. One source of truth is non-negotiable
Derived tables must never drive budget or risk.
4. Debug visibility beats guessing
Adding structured debug output made the root cause obvious in minutes.
5. Cleanup tools are part of architecture
Bugs happen. Systems must be able to heal.
Current State
- Binance open/close logic is correct
- Phantom SHORTs are preventable and cleanable
- Budget calculation is aligned with real state
- Paper Exchange consistency issues are understood and fixable
- Egis architecture is now more explicit, safer, and easier to reason about
