Metadata-Version: 2.5
Name: inflection-exchange
Version: 0.0.4
Summary: Python client for the Inflection exchange. Coming soon.
License-File: LICENSE
License-File: NOTICE
Requires-Python: >=3.11
Requires-Dist: cryptography>=44
Requires-Dist: httpx>=0.28.1
Requires-Dist: websockets>=17.1
Description-Content-Type: text/markdown

# Inflection Python SDK

A Python client for the Inflection exchange. It authenticates with your API
key, keeps the connections open, streams market data, and tracks your orders so
you can place them and await the outcome.

## Install

```sh
pip install inflection-exchange
```

Or straight from GitHub:

```sh
pip install "git+https://github.com/inflection-exchange/inflection-python-sdk.git@v0.1.0"
```

## Quick start

```python
import asyncio
from inflection_exchange import ExchangeClient, MarketDataFeed, Outcome, Side


async def main():
    async with (
        MarketDataFeed() as feed,
        ExchangeClient("your-key-id", "inflection-bot.pem") as client,
    ):
        market = feed.tradable_markets()[0]
        book = feed.books.get((market.market_id, Outcome.UP))
        print(market.slug, book)

        order = await client.place_limit(
            market.market_id, Outcome.UP, Side.BID, 4500, 100
        )
        await order.wait_accepted()
        print(order.status, order.filled_quantity)

        await client.cancel_all()


asyncio.run(main())
```

The client trades for the account its API key belongs to. Create a key in
the app under Settings > API keys > New key: you get a key id, and the
private key downloads as a `.pem` file. The exchange never sees the private
key and can't send it again, so keep the file safe. See the
[API keys guide](https://inflection.exchange/docs/guides/api-keys/).

`ExchangeClient` signs the order socket's handshake with the key. To call the
account API yourself, sign each request the same way:

```python
from inflection_exchange import ApiKey

key = ApiKey("your-key-id", "inflection-bot.pem")
path = "/api/trading/accounts/7/balance"
httpx.get("https://inflection.exchange" + path, headers=key.sign("GET", path))
```

A signature is good for 30 seconds, so keep your clock in sync. If the
exchange refuses the key (a wrong id, a revoked key, or a clock that is off),
the client raises instead of retrying.

The SDK connects to `https://inflection.exchange`.

## Units

| Thing    | Unit          | Notes                                                                                      |
| -------- | ------------- | ------------------------------------------------------------------------------------------ |
| Price    | basis points  | `10_000` = $1 = 100%. Limit prices go from 100 to 9900 in steps of 100.                    |
| Quantity | lots          | 100 lots = 1 share. A winning share pays $1.                                               |
| Outcome  | `up` / `down` | Every market is binary. Buying `down` at p is the same order as selling `up` at 10000 − p. |

A market takes orders from the moment it is listed. While its window hasn't
started it is `open` and has no strike yet. Once the window starts it is
`in_event`, and the strike is known. The market closes when the window ends,
and at that point the exchange cancels every order still resting in it.

## Orders

Orders default to **good-till-cancelled**. Market orders are the exception:
they default to **immediate-or-cancel**, because a market order can't rest on
the book.

```python
order = await client.place_limit(market_id, Outcome.UP, Side.BID, 4500, 200)
order = await client.place_limit(
    market_id, "up", "bid", 4500, 200, expiry_ms=now_ms + 5 * 60_000
)  # good-till-date
order = await client.place_limit(market_id, "up", "ask", 5500, 200, post_only=True)
order = await client.place_market(market_id, "down", "bid", 100)  # IOC
order = await client.place_market(market_id, "down", "bid", 100, TimeInForce.FOK)

await client.modify(order, price_bps=4600)  # new price: goes to the back of the queue
await client.modify(order, quantity=100)  # smaller at the same price: keeps its place
await client.cancel(order)
await client.cancel_all()
```

Each call returns an `Order` that the SDK keeps up to date. You can await it,
or just read it later:

```python
try:
    await order.wait_accepted()  # raises OrderRejectedError if refused
except OrderRejectedError as e:
    if e.reason == RejectReason.INSUFFICIENT_COLLATERAL:
        ...

await order.wait_done()
order.status  # OrderStatus.FILLED, CANCELLED, UNFILLED, REJECTED, ...
order.filled_quantity  # lots filled so far
order.remaining_quantity  # lots still working on the book
order.reason  # why it was rejected, cancelled or unfilled
```

| Status      | Meaning                                                                                     |
| ----------- | ------------------------------------------------------------------------------------------- |
| `PENDING`   | Sent; the exchange hasn't replied yet.                                                      |
| `OPEN`      | Accepted and working. It may be partly filled.                                              |
| `FILLED`    | Completely filled.                                                                          |
| `CANCELLED` | Cancelled. `reason` says why: `client_request`, `expired`, `market_closed` or `self_trade`. |
| `UNFILLED`  | An IOC or market order's remainder was dropped. It may have partly filled first.            |
| `REJECTED`  | Refused. `reason` is a `RejectReason`.                                                      |
| `UNKNOWN`   | Finished while you were disconnected. Your fills say what happened.                         |

`client.orders` holds your live orders. On reconnect it is rebuilt from the
exchange's list of resting orders, so it includes orders from earlier runs.

**Orders are checked before they're sent.** An order the exchange would
certainly refuse raises `InvalidOrder` without being sent: a price that's off
the tick or outside 100–9900, a quantity that isn't a positive whole number
of lots, a resting market order, or a GTD expiry less than a minute ahead.

**The SDK doesn't track positions or P&L.** It passes every fill to you
(`on_fill`), so you can keep whatever view you need. The exchange's account
API holds the authoritative positions and balances.

## Market data

`MarketDataFeed` holds one connection to the public feed and keeps:

- `feed.markets`: every listed market, by id. `feed.tradable_markets()` gives
  the ones taking orders.
- `feed.books[(market_id, outcome)]`: the best bid and ask (`BookTop`).
- `feed.depths[(market_id, outcome)]`: every level, best first (`BookDepth`).

The feed sends every market to every connection, and drops a connection that
falls too far behind. So open one feed per process and share it. To react to
changes as they happen, add a listener with any of these methods:

```python
class Watcher:
    def on_book(self, book): ...
    def on_depth(self, depth): ...
    def on_trade(self, trade): ...  # public trades, anyone's
    def on_market_closed(self, market_id): ...  # trading has stopped
    def on_market_settled(self, market_id, outcome): ...  # outcome is the winner


feed.subscribe(Watcher())
```

Your own order events work the same way. Set `client.listener` to an object
with `on_fill(fill)` and/or `on_order_event(event)`. Listeners are called as
messages arrive, so they should be quick; do slow work in your own tasks.

## Development

```sh
uv sync
uv run pytest
```

`tests/fixtures/*.json` holds a sample of every message the exchange sends.
[test_fixtures.py](tests/test_fixtures.py) checks that the SDK can read all
of them.

## License

Apache License 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
