> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sory.pro/llms.txt
> Use this file to discover all available pages before exploring further.

# Cross-exchange market making

> Quote on one set of venues, hedge on another. About forty lines.

The shape of it: rest a bid on a few venues, and the moment one gets hit, sell the same size across the deepest venues you have. You earn the spread; SORY finds the best place to take the other side.

Everything here uses one client, because it is one symbol.

```python theme={"dark"}
import asyncio
from decimal import Decimal

from sory import SoryClient

QUOTE_ON = ["bullish", "coinbaseexchange", "kraken"]   # where the bid rests
HEDGE_ON = ["binance", "bybit", "okx"]                 # where the hedge crosses

config = {
    "licence_key": "...",
    "symbol": "BTC/USDT",
    "exchanges": QUOTE_ON + HEDGE_ON,
    "fees_bps": "fees.toml",
    # The hedge crosses the spread, so compare hedge venues on what you actually pay.
    "fee_offset": {venue: "taker" for venue in HEDGE_ON},
}

SIZE = Decimal("0.05")
EDGE_BPS = Decimal("8")


async def main():
    async with SoryClient(config) as sory:
        await asyncio.sleep(1)                     # let the books fill

        book = sory.book()
        mid = (book.best_bid.price + book.best_ask.price) / 2
        price = mid - (mid * EDGE_BPS / 10000)

        # One resting bid per venue. Limits go to one venue each — they are never split.
        quotes = {}
        for venue in QUOTE_ON:
            order = await sory.place(
                side="buy",
                qty=SIZE,
                exchanges=[venue],
                order_type="limit",
                price=price,
                post_only=True,                    # never cross; keep the maker side
            )
            quotes[order.children[0].client_order_id] = order

        # When one is hit, sell what was filled across the deep venues.
        hedged: dict[str, Decimal] = {}
        async for update in sory.order_updates():
            order = quotes.get(update.client_order_id)
            if order is None:
                continue                           # someone else's order on the same key

            new_fill = order.filled - hedged.get(order.id, Decimal(0))
            if new_fill <= 0:
                continue

            hedged[order.id] = order.filled
            hedge = await sory.place(
                side="sell",
                qty=new_fill,
                order_type="market",
                exchanges=HEDGE_ON,
            )
            print(f"hit {new_fill} on {update.venue}, hedged at {hedge.achieved_price}")


asyncio.run(main())
```

## Why it is only this long

<Columns cols={2}>
  <Card title="The order updates itself" icon="repeat">
    `order.filled` is live. You hold the object `place()` gave you and read it — no lookup, no bookkeeping.
  </Card>

  <Card title="The hedge picks its own venue" icon="split">
    A market order across `HEDGE_ON` walks the merged book and takes the cheapest liquidity, wherever it is.
  </Card>

  <Card title="Fees are in the comparison" icon="percent">
    `fee_offset` on the hedge venues means "cheapest" accounts for what each one charges you.
  </Card>

  <Card title="Dead venues drop out" icon="wifi-off">
    A venue that stops publishing leaves the book on its own, so the hedge never routes into a stale price.
  </Card>
</Columns>

<Warning>
  **Keep the two sets apart.** If you rest a bid on a venue and then send a sell there, you can trade against yourself. Some venues prevent it on the same account and some do not, so the reliable fix is the one above: hedge only on venues you are not quoting on.

  Overlapping them is fine only if you have confirmed that venue's self-trade prevention yourself.
</Warning>

## Picking up after a restart

SORY keeps nothing on disk. Ask the venues what you still have working:

```python theme={"dark"}
async with SoryClient(config) as sory:
    for resting in await sory.open_orders():
        print(resting.venue, resting.id, resting.price, resting.remaining)
```

Cancel them and re-quote, or adopt them — either way you are looking at the truth rather than a file that might be stale.

```python theme={"dark"}
await sory.cancel_all()      # everything resting for this symbol, every venue
```

## Before you point it at real money

```python theme={"dark"}
config = {**config, "dry_run": True}
```

Every path runs — quoting, the fill loop, routing the hedge, per-venue rounding — and nothing is sent. Watch the printed hedges for a while first.

<Note>
  `post_only` and `time_in_force` are refused on any venue that has not proven it supports them, rather than sent without. Check the [venues table](/venues) before you rely on either.
</Note>
