System Design

10 min read

May 27, 2026

Broker Integrations: Order Execution and Trade Protection in Alpaca and Binance

How the platform integrates with Alpaca for equity trading and Binance for crypto, the validation layer that sits between model signals and submitted orders, and how a local ledger catches discrepancies between what the system intended and what the broker actually executed.

PythonAlpacaBinanceTradingSystem Design

The two-broker architecture

Alpaca handles US equities. Binance handles crypto. The split was driven by fees first.

Alpaca charges 0.25% per crypto trade. Binance charges 0.10%. At the position sizing and signal frequency the models generate, that 60% fee reduction is not a marginal improvement. It is the difference between a strategy that produces alpha and one that the fee structure consumes. Once the math was clear, routing crypto through Binance was the only reasonable choice.

For equities, Alpaca is commission-free and has a paper trading environment that mirrors the live API exactly. The feedback loop from paper to live is clean: same API shapes, same fill behavior, no surprises in the transition. Fractional share support matters for position sizing at small experiment scale.

Binance also has better liquidity and tighter spreads for crypto. The trade-off is a more complex API with more surface area for configuration errors.

Both integrations sit behind a shared ExecutionClient interface. Signals from the model layer flow into the interface. The interface dispatches to the appropriate broker based on the asset class of the symbol. Neither the model layer nor the signal generator knows which broker is being used.

Paper trading as the required first step

No model moves directly from gate-passing to live trading. Paper trading is mandatory first.

Alpaca's paper environment is a close approximation of live trading. Orders are filled at live market prices. The API returns the same response shapes as live. Position tracking behaves identically. The main difference is that no real money changes hands. A model can run in paper mode for days or weeks and produce a realistic assessment of execution behavior before any live capital is at risk.

Binance does not have a built-in paper trading mode for futures. The platform handles this by running the execution layer against a simulated order book that fills orders at live market prices but tracks positions locally rather than submitting to Binance. This is less faithful than Alpaca's paper mode but sufficient for validating that the signal-to-order path is working correctly.

Paper trading also surfaces execution timing issues that backtesting cannot. A backtest assumes you can trade at the close price of the bar that generated the signal. In practice there is latency between signal generation and order submission, and orders may fill at a worse price than the assumed entry. Paper trading with live prices makes this visible before it affects a real position.

Order validation before submission

Every order passes through a validation layer before it reaches the broker. The validation layer runs four checks.

Position size: the order must not exceed the configured maximum position size for the experiment, expressed as a fraction of total portfolio value. Duplicate position: if the portfolio already holds a position in the symbol, a new entry order is rejected. The platform does not add to existing positions. Exposure limit: total open position value must not exceed the configured maximum total exposure. Price sanity: for limit orders, the limit price is compared against the last known market price. An order priced more than a configurable percentage away from the current market is likely a stale signal or a calculation error.

execution/validator.py
class OrderRejection(Exception):
    pass

def validate_order(
    request: OrderRequest,
    portfolio: PortfolioState,
    config: dict,
) -> None:
    max_position_pct  = config["position"]["size"]
    max_exposure_pct  = config["position"].get("max_exposure", 0.5)
    max_price_dev_pct = config["position"].get("price_deviation_pct", 0.03)

    position_value = request.qty * portfolio.last_price(request.symbol)
    position_pct   = position_value / portfolio.total_value

    if position_pct > max_position_pct:
        raise OrderRejection(
            f"position size {position_pct:.1%} exceeds limit {max_position_pct:.1%}"
        )

    if request.side == "buy" and portfolio.has_position(request.symbol):
        raise OrderRejection(f"duplicate position: already holding {request.symbol}")

    current_exposure = portfolio.total_position_value / portfolio.total_value
    if current_exposure + position_pct > max_exposure_pct:
        raise OrderRejection(
            f"exposure limit exceeded: current {current_exposure:.1%} + new {position_pct:.1%}"
        )

    if request.order_type == "limit" and request.limit_price is not None:
        last_price = portfolio.last_price(request.symbol)
        deviation  = abs(request.limit_price - last_price) / last_price
        if deviation > max_price_dev_pct:
            raise OrderRejection(
                f"stale limit price: {deviation:.1%} from market"
            )

Rejected orders are logged with their rejection reason. This creates a record of signals that were valid from the model's perspective but blocked by the execution layer, which is useful for diagnosing whether position sizing or exposure limits are misaligned with the experiment's expected signal frequency.

The local ledger

The local ledger is a PostgreSQL table that tracks every order the platform has submitted and what the broker reported back. Its purpose is to maintain the platform's own record of what it believes its positions to be, independently of the broker's representation.

Every submitted order is written to the ledger with a pending status. When the broker confirms execution, the ledger is updated with the actual fill price and quantity. Before any new order is submitted, the ledger is reconciled against the broker's current positions.

execution/ledger.py
def reconcile(
    broker_positions: dict[str, float],
    ledger_positions: dict[str, float],
    tolerance: float = 0.001,
) -> list[str]:
    discrepancies = []
    all_symbols = set(broker_positions) | set(ledger_positions)

    for symbol in all_symbols:
        broker_qty = broker_positions.get(symbol, 0.0)
        ledger_qty = ledger_positions.get(symbol, 0.0)
        delta      = broker_qty - ledger_qty

        if abs(delta) > tolerance:
            discrepancies.append(
                f"{symbol}: broker={broker_qty:.4f}, "
                f"ledger={ledger_qty:.4f}, delta={delta:+.4f}"
            )

    return discrepancies

def assert_parity(broker_positions, ledger_positions) -> None:
    discrepancies = reconcile(broker_positions, ledger_positions)
    if discrepancies:
        # Halt order submission until manually resolved
        raise LedgerParityError(
            "position mismatch detected before order submission:\n"
            + "\n".join(discrepancies)
        )

What discrepancies tell you

Any discrepancy between the ledger and the broker halts new order submission and triggers an alert. The system should not continue trading if what it believes about its positions does not match what the broker reports. Discrepancies arise from partial fills, order timeouts, broker-side rejections that were not communicated cleanly, and network interruptions during order confirmation. The right response in all cases is to stop, reconcile manually, and restart from a known-good state.

The ledger also provides an audit trail for every position the platform has ever held. This is important for analysing execution performance separately from model performance. A model with a strong backtest Sharpe that shows degraded performance in paper trading is worth investigating at the ledger level. The gap between expected entry price from the backtest and actual fill price from the ledger is often part of the story. Slippage on entry compounds across many trades and is invisible until you have a ledger to measure it against.