Metadata-Version: 2.4
Name: topstep-sdk
Version: 0.1.2
Summary: High-performance, async-first Python SDK for the Topstep (TopstepX / ProjectX Gateway) trading API.
Project-URL: Homepage, https://github.com/tarricsookdeo/topstep-sdk
Project-URL: Repository, https://github.com/tarricsookdeo/topstep-sdk
Project-URL: Documentation, https://github.com/tarricsookdeo/topstep-sdk#readme
Project-URL: Issues, https://github.com/tarricsookdeo/topstep-sdk/issues
Author-email: Tarric Sookdeo <tarricsookdeo@outlook.com>
License-Expression: MIT
License-File: LICENSE
Keywords: async,futures,projectx,sdk,topstep,topstepx,trading
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: httpx>=0.27
Requires-Dist: msgspec>=0.18
Requires-Dist: websockets>=13.0
Provides-Extra: dev
Requires-Dist: pyright>=1.1.380; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
Requires-Dist: mkdocstrings[python]>=0.26; extra == 'docs'
Description-Content-Type: text/markdown

# topstep-sdk

[![CI](https://github.com/tarricsookdeo/topstep-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/tarricsookdeo/topstep-sdk/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

A **high-performance, async-first** Python SDK for the Topstep trading API
(TopstepX, powered by the ProjectX Gateway API).

> **Unofficial.** This project is not affiliated with, endorsed by, or sponsored
> by Topstep, LLC or ProjectX Trading, LLC. "Topstep" and "TopstepX" are
> trademarks of their respective owners.

> **Building with an AI agent?** See [`AGENTS.md`](AGENTS.md) — a complete
> capability map plus the non-obvious trading semantics (order side, tick-offset
> brackets, the `live` flag, result ordering) that prevent common mistakes.

## Why this SDK

- **Async-first core** built on `httpx`, with a **synchronous facade** for plain scripts.
- **Fast models** via [`msgspec`](https://jcristharif.com/msgspec/) — minimal overhead on high-throughput quote and depth streams.
- **Realtime** SignalR client for both the **user hub** (accounts, orders, positions, trades) and **market hub** (quotes, trades, depth), with auto-reconnect and resubscribe.
- **Fully typed** (`py.typed`), tiny dependency footprint (`httpx`, `msgspec`, `websockets`).

## Status

🚧 **Alpha / under active development.** The public API may change before `1.0`.

## Requirements

- Python **3.12+**
- A TopstepX account and an API key (TopstepX → Settings → API).

## Installation

```bash
pip install topstep-sdk
```

Provide credentials as **process environment variables** — `from_env()` reads
`os.environ` and does **not** auto-load a `.env` file (export them, or load a
`.env` yourself first):

```bash
export TOPSTEP_USERNAME=your_username
export TOPSTEP_API_KEY=your_api_key
```

Or pass them directly: `AsyncTopstepClient(username="...", api_key="...")`.

## Quickstart (async)

This quickstart is **read-only** — it never places an order:

```python
import asyncio
from datetime import datetime, timedelta, timezone
from topstep_sdk import AsyncTopstepClient, AggregateBarUnit


async def main() -> None:
    async with AsyncTopstepClient.from_env() as client:  # TOPSTEP_USERNAME / TOPSTEP_API_KEY
        accounts = await client.accounts.search()          # NOTE: may return >1 tradable account
        account = accounts[0]                              # ...select deliberately in real code

        contracts = await client.contracts.search("MNQ")   # fuzzy; pick the intended contract
        mnq = contracts[0]

        now = datetime.now(timezone.utc)
        bars = await client.history.retrieve_bars(
            mnq.id, unit=AggregateBarUnit.MINUTE, unit_number=5,
            start_time=now - timedelta(hours=2), end_time=now, limit=50,
        )
        print(account.name, account.balance, "| last close:", bars[0].close)


asyncio.run(main())
```

### Placing orders

> ⚠️ **This sends a REAL order to your Topstep account.** Test on a
> practice/evaluation account with `size=1`. Remember `accounts.search()` may
> return more than one tradable account — pick the right one deliberately.

```python
# Market buy 1 with a protective OCO bracket. Pass tick DISTANCES (magnitudes) —
# the SDK signs them for the side (stop-loss below a long, take-profit above):
order_id = await client.orders.buy(
    account.id, mnq.id, size=1,
    stop_loss_ticks=40, take_profit_ticks=80,
)
# place() returns immediately; the fill arrives via orders.get(...) or the user hub.
await client.positions.close(account.id, mnq.id)  # flatten the whole contract position
```

(For exact control you can instead pass the raw, **signed** `stop_loss_bracket=` /
`take_profit_bracket=` via `PlaceOrderBracket` — see [`AGENTS.md`](AGENTS.md).)

### Synchronous

```python
from topstep_sdk import TopstepClient

with TopstepClient.from_env() as client:
    for account in client.accounts.search():
        print(account.id, account.name, account.balance)
```

### Realtime

```python
async with AsyncTopstepClient.from_env() as client:
    market = client.market_hub()

    @market.on_quote
    def _(contract_id: str, quote) -> None:
        print(contract_id, quote.best_bid, quote.best_ask)

    await market.start()
    await market.subscribe_quotes("CON.F.US.MNQ.U26")
    await asyncio.sleep(30)
    await market.stop()

    # User hub: client.user_hub() → on_account / on_order / on_position / on_trade,
    # subscribe_orders(account_id), etc. Auto-reconnects and re-subscribes.
```

## Error handling

Everything the SDK raises derives from `TopstepError`, so `except TopstepError`
catches the whole surface. **Domain rejections** (bad order, insufficient funds,
not-found, …) surface as `APIError` with a numeric `error_code`;
transport/auth/rate-limit failures have their own types.

```python
from topstep_sdk import APIError, AuthError, RateLimitError, OrderSide, OrderType
from topstep_sdk.enums import PLACE_ORDER_ERRORS

try:
    order_id = await client.orders.place(
        account.id, mnq.id, side=OrderSide.BUY, type=OrderType.MARKET, size=1
    )
except RateLimitError as e:
    ...  # back off; e.retry_after may be set
except AuthError:
    ...  # bad or expired credentials
except APIError as e:
    name = PLACE_ORDER_ERRORS.get(e.error_code or -1, "Unknown")
    ...  # e.g. "InsufficientFunds", "OutsideTradingHours", "AccountViolation"
```

| Exception | Raised when |
|---|---|
| `AuthError` | 401/403, bad key, login/validate failure |
| `RateLimitError` | HTTP 429 after retries (`.retry_after`) |
| `NotFoundError` | HTTP 404 |
| `ServerError` | HTTP 5xx after retries |
| `APIError` | domain rejection (`success:false`); base of the above (`.error_code`) |
| `TransportError` | network/timeout after retries |
| `RealtimeError` | SignalR connect/protocol error |
| `ConfigError` | missing/invalid credentials |

Lookup methods that model "not found" as a value return `None` instead of raising:
`contracts.search_by_id(...)` and `orders.get(...)`.

## Coverage

Full REST surface — **Account**, **Contract** (search / by-id / available), **History**
(bars), **Order** (search / open / by-id / paginated query / place / modify / cancel),
**Position** (open / close / partial-close), **Trade** (search) — plus both realtime
**user** and **market** hubs. Auth (API-key login, session validate/refresh), client-side
token-bucket rate limiting (50/30s for history, 200/60s otherwise), retry/backoff, and
typed error mapping are built in. Verified against the live TopstepX API; see
[`docs/RECONCILIATION.md`](docs/RECONCILIATION.md) for spec-vs-reality notes.

## Roadmap

- [x] Project scaffold, config, error hierarchy, CI
- [x] Auth + HTTP core (login/refresh, rate limiting, retries)
- [x] REST resources (accounts, contracts, history, orders, positions, trades)
- [x] Realtime SignalR (user + market hubs)
- [x] Synchronous facade
- [x] First PyPI release — `pip install topstep-sdk`
- [ ] Live-verify bracket/OCO + trailing-stop placement when markets reopen (see [`docs/TESTING.md`](docs/TESTING.md))
- [ ] Docs site

## Development

```bash
uv sync --extra dev
uv run pytest
uv run ruff check .
uv run pyright
```

## License

[MIT](LICENSE)
