Egis Technical Journal

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 (or role):
    • open
    • close
  • 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 = 0 or > 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 = binance
  • side = short
  • status = open
  • There exists a CLOSED LONG with:
    • same bot
    • same symbol
  • abs(short.created_at − long.closed_at) < 3 seconds
  • short.entry_price == long.exit_price
  • short.size_usd == long.size_usd

The command supports:

  • --debug (full reasoning output)
  • dry-run by default
  • --confirm to 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 positions table 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 positions and paper_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