Agent skill

position-reconciliation

Broker position reconciliation pattern for live trading systems

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/devops/position-reconciliation

SKILL.md

Position Reconciliation - Research Notes

Experiment Overview

Item Details
Date 2025-12-16
Goal Sync internal position state with broker to prevent state drift
Environment Python 3.10, Alpaca API
Status Success

Context

Live trading systems maintain internal position state that can drift from reality:

  • Orders execute but callback/update fails
  • Manual interventions via broker UI
  • Partial fills not properly tracked
  • Network issues during order submission
  • Broker-side corporate actions (splits, mergers)

Without reconciliation, the system may try to close positions it doesn't have or miss positions it should manage.

Verified Workflow

1. Reconciliation Function

python
def reconcile_positions(
    broker: Broker,
    states: Dict[str, PositionState],
    logger: logging.Logger,
) -> Dict[str, PositionState]:
    """
    Reconcile internal position state with broker's actual positions.

    Broker is source of truth - any discrepancies update internal state.
    """
    try:
        broker_positions = broker.get_positions()
        broker_symbols = set()

        for pos in broker_positions:
            symbol = pos.get('symbol', '')
            if not symbol:
                continue
            broker_symbols.add(symbol)

            # Parse broker position
            qty = float(pos.get('qty', 0))
            side = 1 if qty > 0 else (-1 if qty < 0 else 0)
            avg_entry = float(pos.get('avg_entry_price', 0))
            unrealized_pl = float(pos.get('unrealized_pl', 0))

            # Check if we have internal state
            if symbol in states:
                internal = states[symbol]

                # Check for quantity or side mismatch
                if abs(abs(qty) - internal.shares) > 0.001 or internal.side != side:
                    logger.warning(
                        f"RECONCILE {symbol}: Internal={internal.side}x{internal.shares:.4f} vs "
                        f"Broker={side}x{abs(qty):.4f} - updating to broker"
                    )
                    states[symbol] = PositionState(
                        side=side,
                        shares=abs(qty),
                        entry_price=avg_entry,
                        unrealized_pnl=unrealized_pl,
                        trade_count=internal.trade_count,  # Preserve history
                    )
            else:
                # Broker has position we don't track
                if qty != 0:
                    logger.warning(
                        f"RECONCILE {symbol}: Found untracked broker position "
                        f"{side}x{abs(qty):.4f} @ {avg_entry:.2f} - adding to state"
                    )
                    states[symbol] = PositionState(
                        side=side,
                        shares=abs(qty),
                        entry_price=avg_entry,
                        unrealized_pnl=unrealized_pl,
                        trade_count=0,
                    )

        # Check for internal positions broker doesn't have
        for symbol, internal in list(states.items()):
            if internal.side != 0 and symbol not in broker_symbols:
                logger.warning(
                    f"RECONCILE {symbol}: Internal shows {internal.side}x{internal.shares:.4f} "
                    f"but broker has no position - clearing internal state"
                )
                states[symbol] = PositionState(trade_count=internal.trade_count + 1)

        return states

    except Exception as e:
        logger.error(f"Position reconciliation failed: {e}")
        return states  # Return unchanged on error

2. Integration in Trading Loop

python
# Run reconciliation periodically (every 30 min = 120 loops at 15s interval)
RECONCILIATION_INTERVAL = 120

while running:
    loop_count += 1

    # ... trading logic ...

    # Position reconciliation
    if loop_count % RECONCILIATION_INTERVAL == 0:
        logger.info("Running position reconciliation with broker...")
        states = reconcile_positions(broker, states, logger)

    time.sleep(15)

3. Broker API Response Format (Alpaca)

python
# Example broker.get_positions() response
[
    {
        'symbol': 'AAPL',
        'qty': '10.5',           # Supports fractional
        'avg_entry_price': '150.25',
        'current_price': '152.00',
        'unrealized_pl': '18.375',
        'market_value': '1596.00',
        'side': 'long'
    },
    ...
]

Failed Attempts (Critical)

Attempt Why it Failed Lesson Learned
Only reconciling on startup Drift accumulates over hours Run periodically (every 30 min)
Internal state as source of truth Broker has the real positions Broker is ALWAYS truth
Not handling fractional shares qty = 10.5 parsed as 10 Use float(), not int()
Raising exception on mismatch System stops on first discrepancy Log warning, fix, continue
Clearing trade_count on reconcile Lose performance history Preserve trade_count field
Not handling new broker positions Miss manual buys via UI Add unknown positions to state
Exact quantity match only Floating point issues Use tolerance (0.001 shares)

Final Parameters

python
# Recommended reconciliation settings
RECONCILIATION_INTERVAL = 120  # loops (30 min at 15s interval)
QUANTITY_TOLERANCE = 0.001     # shares - for floating point comparison
LOG_LEVEL = logging.WARNING    # Log discrepancies prominently

Key Insights

  • Broker is ALWAYS source of truth - Never override broker with internal state
  • Run periodically, not just on startup - State can drift anytime
  • Preserve metadata on reconcile - Keep trade_count, strategy attribution
  • Handle three cases: Mismatch, untracked broker position, phantom internal position
  • Use tolerance for float comparison - Exact equality fails with fractional shares
  • Log discrepancies prominently - RECONCILE prefix for easy grep
  • Don't raise on errors - Log and continue with existing state
  • Reconcile BEFORE trading logic - Ensure state is accurate before decisions

Edge Cases to Handle

python
# 1. Partial fills
#    Internal: 100 shares, Broker: 75 shares
#    -> Update internal to 75, order may still be working

# 2. Stock splits
#    Internal: 10 shares @ $100, Broker: 40 shares @ $25
#    -> Trust broker, update shares and price

# 3. Manual close via broker UI
#    Internal: 50 shares, Broker: 0 shares
#    -> Clear internal state, increment trade_count

# 4. Zero quantity edge case
#    Broker returns qty=0 for recently closed position
#    -> Don't add to tracking, just skip

Testing Checklist

python
# Verify these scenarios:
1. [ ] Internal 100 shares, broker 100 shares -> No change
2. [ ] Internal 100 shares, broker 50 shares -> Update to 50
3. [ ] Internal 0 shares, broker 100 shares -> Add new position
4. [ ] Internal 100 shares, broker 0 shares -> Clear internal
5. [ ] Internal long, broker short -> Flip side
6. [ ] Broker API fails -> Return unchanged state
7. [ ] Fractional shares (10.5) handled correctly

References

  • Alpaca API: client.get_all_positions()
  • Best practice: Reconcile every 15-60 minutes in production
  • Related: Order fill tracking, execution confirmation

Didn't find tool you were looking for?

Be as detailed as possible for better results