position-reconciliation

smith6jt-cop's avatarfrom smith6jt-cop

Broker position reconciliation pattern for live trading systems

⭐0stars🔀0forks📁View on GitHub🕐Updated Jan 3, 2026

When & Why to Use This Skill

This Claude skill provides a professional-grade architectural pattern for reconciling internal position states with broker data in live trading systems. It solves the critical problem of 'state drift'—where internal records diverge from actual holdings due to network issues, manual trades, or partial fills—by establishing the broker as the absolute source of truth and providing a robust synchronization workflow.

Use Cases

  • Automated State Synchronization: Periodically verifying internal trading bot states against broker APIs (like Alpaca or Interactive Brokers) to prevent over-leveraging or execution errors.
  • Manual Intervention Recovery: Detecting and adjusting internal records when a trader manually modifies positions via the broker's web or mobile interface.
  • Handling Corporate Actions: Automatically updating position quantities and entry prices following stock splits, mergers, or other broker-side adjustments that don't trigger standard trade callbacks.
  • Resilient Error Handling: Maintaining system stability by recovering accurate position data after network timeouts, system crashes, or missed API webhooks during high-volatility periods.
nameposition-reconciliation
description"Broker position reconciliation pattern for live trading systems"
authorClaude Code
date2025-12-16

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

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

# 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)

# 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

# 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

# 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

# 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