Metadata-Version: 2.4
Name: rithmic-rapi
Version: 0.2.0
Summary: Native async Python client for the Rithmic R|API.
Project-URL: Homepage, https://github.com/alexandre-meline/pyrithmic
Project-URL: Repository, https://github.com/alexandre-meline/pyrithmic
Project-URL: Documentation, https://github.com/alexandre-meline/pyrithmic/tree/main/docs
Project-URL: Issues, https://github.com/alexandre-meline/pyrithmic/issues
Project-URL: Changelog, https://github.com/alexandre-meline/pyrithmic/blob/main/CHANGELOG.md
Author-email: Alexandre Méline <alexandre.meline.dev@gmail.com>
Maintainer-email: Alexandre Méline <alexandre.meline.dev@gmail.com>
License: Apache-2.0
License-File: LICENSE
Keywords: async,broker,futures,protobuf,rithmic,trading,websocket
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: protobuf<6,>=5.28
Requires-Dist: pydantic>=2.9
Requires-Dist: structlog>=24.1
Requires-Dist: websockets>=13.1
Provides-Extra: dev
Requires-Dist: grpcio-tools>=1.66; extra == 'dev'
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: pyright>=1.1.380; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-benchmark>=4.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest-timeout>=2.3; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: python-dotenv>=1.0; extra == 'dev'
Requires-Dist: ruff>=0.7; extra == 'dev'
Provides-Extra: fast
Requires-Dist: uvloop>=0.19; (sys_platform != 'win32') and extra == 'fast'
Description-Content-Type: text/markdown

# pyrithmic

[![CI](https://github.com/alexandre-meline/pyrithmic/actions/workflows/ci.yml/badge.svg)](https://github.com/alexandre-meline/pyrithmic/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/rithmic-rapi.svg)](https://pypi.org/project/rithmic-rapi/)
[![Python](https://img.shields.io/pypi/pyversions/rithmic-rapi.svg)](https://pypi.org/project/rithmic-rapi/)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)

> Distribution: **`rithmic-rapi`** on PyPI · import name: **`pyrithmic`**.

**Native async Python client for the Rithmic R|API.** Fully typed, fail-loud, and built around a single consistent contract.

`pyrithmic` is an `asyncio`-only client for the Rithmic R|API protocol (WebSocket + Protocol Buffers). Every callback delivers a **typed, frozen [Pydantic](https://docs.pydantic.dev/) model** (never a raw dict or a bare protobuf message), so a consumer writes the same code for a fill, a tick, a PnL push, or a historical bar. The public surface is strict, explicit, and verified under `pyright --strict`.

```python
import asyncio
from pyrithmic import RithmicClient
from pyrithmic.protocol.plants import Plant
from pyrithmic.models.enums import MarketDataType
from pyrithmic.models.market_data import LastTrade

async def main() -> None:
    # Reads RITHMIC_USER / RITHMIC_PASSWORD / RITHMIC_SYSTEM_NAME / RITHMIC_GATEWAY_URL
    client = RithmicClient.from_env(plants=frozenset({Plant.TICKER}))

    @client.on_last_trade
    async def _on_trade(trade: LastTrade) -> None:   # a typed model, with full IDE autocomplete
        print(trade.symbol, trade.trade_price, trade.trade_size)

    async with client:                                # connects, logs in, reconnects on drop
        contract = await client.get_front_month_contract("NQ", "CME")
        await client.subscribe_market_data(contract.trading_symbol, "CME", MarketDataType.LAST_TRADE)
        await asyncio.sleep(10)

asyncio.run(main())
```

---

## Highlights

- **One consistent, typed contract.** Every push and every response is decoded into a frozen Pydantic v2 model before it reaches your code. There is no `dict` in one place and a protobuf message in another: the same typed, immutable surface everywhere.
- **Strict, validated signatures.** Orders are submitted as validated `OrderRequest` models (no opaque `**kwargs`); invalid input is rejected at construction, before it ever touches the wire.
- **Fail-loud error model.** A `PyrithmicError` hierarchy maps server rejects to named exceptions (`OrderRejected`, `RpcRejected`, `LoginRejected`, and so on). No silent fallbacks, no swallowed errors.
- **Correct connection topology.** One WebSocket connection per plant, the only topology the R|API actually supports, multiplexed transparently behind a single client.
- **Resilient by design.** Per-plant reconnect with configurable backoff and a finite retry budget; active subscriptions are **replayed automatically** after a reconnect.
- **Race-free RPC layer.** UUID-correlated request/response matching, explicit terminal-response detection, idempotent push de-duplication, and lock-protected shared state.
- **Broad protocol coverage.** Orders (flat / bracket / OCO, modify with fill-race handling, cancel), PnL (snapshot + live stream), market data (quotes, statistics, front-month), and historical bars (replay + live stream).
- **Structured, opt-in observability.** `structlog`-based logging that is silent by default and never logs secrets; your application stays in control of sinks and levels.
- **Strict typing and lean dependencies.** Ships `py.typed`, passes `pyright --strict`, and depends only on `protobuf`, `websockets`, `pydantic`, and `structlog`.
- **Live-gated.** Every feature is validated against a live Rithmic gateway across the order, PnL, market-data and historical workflows before it is shipped.

---

## Installation

```bash
pip install rithmic-rapi        # or:  uv add rithmic-rapi
```

The PyPI distribution is `rithmic-rapi`; the import name stays `pyrithmic` (`import pyrithmic`).

Requires **Python 3.11+**.

An optional `fast` extra installs [`uvloop`](https://github.com/MagicStack/uvloop) (a faster event loop) on non-Windows platforms:

```bash
pip install "rithmic-rapi[fast]"
```

pyrithmic stays event-loop-agnostic and never changes the global loop policy itself. Installing the extra only makes `uvloop` available on your platform; you enable it explicitly in your own entry point (see [ADR-0006](docs/adr/0006-uvloop-optional-extra.md)):

```python
import asyncio
import uvloop

asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
# ...then run the pyrithmic client on the uvloop-backed loop
```

---

## Usage

### Connecting

Provide credentials explicitly, or load them from the environment with `from_env`. The client is an async context manager: entering it opens the connections, logs into every requested plant, and starts heartbeat and reconnect supervision; leaving it shuts everything down gracefully.

```python
from pydantic import SecretStr
from pyrithmic import RithmicClient, RithmicCredentials
from pyrithmic.protocol.plants import Plant

creds = RithmicCredentials(
    user=SecretStr("my-user"),
    password=SecretStr("my-password"),
    system_name="Rithmic Paper Trading",
    gateway_url="rituz00100.rithmic.com:443",   # "wss://" is added for you; an explicit wss:// is also accepted
    app_name="my-app",
    app_version="1.0.0.0",
)

client = RithmicClient(creds, plants=frozenset({Plant.ORDER, Plant.PNL}))
```

Each plant is opt-in. A market-data-only client (`{Plant.TICKER}`) or a monitoring-only client (`{Plant.PNL}`) logs into just that plant, so you never pay for capabilities you don't use.

### Submitting an order

Orders are validated `OrderRequest` models. You usually want to route to the **current front-month contract**, so resolve the canonical underlying first (for example `NQ` to `NQM6`) and feed the resolved symbol into the request. `submit_and_wait` then returns the terminal `OrderUpdate` (filled or cancelled), so the happy path is a single `await`:

```python
from pyrithmic.models.order import OrderRequest
from pyrithmic.models.enums import TransactionType, OrderType, OrderPlacement

async with client:                          # client built with Plant.ORDER and Plant.TICKER
    # Resolve the underlying to the current front-month contract (e.g. "NQ" -> "NQM6").
    contract = await client.get_front_month_contract("NQ", "CME")

    req = OrderRequest(
        account_id="ACCOUNT-123",
        symbol=contract.trading_symbol,         # the resolved front-month symbol
        exchange=contract.trading_exchange,
        transaction_type=TransactionType.BUY,
        order_type=OrderType.LIMIT,
        quantity=1,
        price=21_000.0,
        placement=OrderPlacement.MANUAL,        # wire manual_or_auto; AUTO for automated routing
    )

    fill = await client.submit_and_wait(req, timeout=30.0)
    print(fill.status, fill.fill_price)         # OrderStatus.FILLED, 21000.0
```

Resolving the front-month contract requires `Plant.TICKER` in the client's plant set. A malformed request raises at construction; a gateway rejection raises `OrderRejected` with the reason, never a silent no-op.

### Streaming PnL

```python
from pyrithmic.models.pnl import AccountPnL

@client.on_account_pnl_update
async def _on_pnl(pnl: AccountPnL) -> None:
    print(pnl.account_id, pnl.day_pnl, pnl.account_balance)

async with client:
    await client.subscribe_pnl_updates("ACCOUNT-123")
    account, positions = await client.get_pnl_snapshot("ACCOUNT-123")
    await asyncio.sleep(60)
```

### Historical bars

```python
from datetime import datetime, timezone
from pyrithmic.models.enums import TimeBarType

async with client:                          # client built with Plant.HISTORY
    bars = await client.replay_time_bars(
        "NQM6", "CME",
        bar_type=TimeBarType.MINUTE_BAR, period=1,
        start=datetime(2026, 6, 1, 14, 0, tzinfo=timezone.utc),
        finish=datetime(2026, 6, 1, 15, 0, tzinfo=timezone.utc),
    )
    for bar in bars:                        # each is a typed TimeBar
        print(bar.bar_start, bar.open_price, bar.high_price, bar.low_price, bar.close_price)
```

### Logging

Logging is silent by default. Opt in from your application with one call (console for humans, JSON for ingestion), and pyrithmic never logs credentials:

```python
from pyrithmic.observability.logging import configure_logging

configure_logging(level="INFO")              # or: configure_logging(level="DEBUG", json=True)
```

---

## Architecture

`pyrithmic` is layered, with a strict boundary between the wire and the public API:

| Layer | Responsibility |
|---|---|
| `transport/` | WebSocket connection, framing, heartbeat, reconnect |
| `protocol/` | Protobuf codec and the `Template` enum (wire layer) |
| `session/` | Login handshake, RPC correlation, response semantics |
| `plants/` | ORDER / PNL / TICKER / HISTORY / ADMIN domain logic |
| `models/` | Frozen Pydantic v2 models, the typed public surface |
| `client.py` | `RithmicClient`, the high-level facade |

The protobuf bindings are an internal detail: application code never imports them directly and never sees a raw protobuf message. Design decisions are recorded as [Architecture Decision Records](docs/adr/) and the full layout lives in [`docs/architecture.md`](docs/architecture.md).

### Capabilities

| Plant | Coverage |
|---|---|
| **Order** | submit · bracket · OCO · modify (fill-race aware) · cancel · fills · RMS · order history |
| **PnL** | account & instrument snapshot · live position/PnL stream |
| **Ticker** | front-month resolution · last-trade / best-bid-offer · trade & quote statistics |
| **History** | time / tick / volume-profile bar replay · live bar stream |
| **Admin** | account discovery · pre-login system & gateway discovery · reference data |

---

## Development

```bash
uv sync                  # install dependencies + create .venv
just test                # unit tests
just test-integration    # live Rithmic tests (require RITHMIC_* env vars)
just lint                # ruff check
just typecheck           # pyright --strict
just proto               # regenerate protobuf bindings
just build               # build wheel + sdist
just ci                  # lint + format-check + typecheck + tests
```

The public API is unstable before `0.1.0`: breaking changes may land between pre-releases and are recorded in [`CHANGELOG.md`](CHANGELOG.md).

---

## License

`pyrithmic` (transport, codec wrappers, plant abstractions, models, and the client facade) is released under the [Apache License 2.0](LICENSE).

## Disclaimer & trademark notice

This project is **not affiliated with, endorsed by, or sponsored by Rithmic, LLC** or Trading Technologies International. "Rithmic" and "R|API" are trademarks of their respective owners.

### Protocol bindings

The Python protobuf bindings shipped in the PyPI wheel (`pyrithmic.protocol._generated`) are compiled from Rithmic's R|API protocol distribution. The Rithmic `.proto` source files themselves are **not** redistributed in this repository or in the wheel; they remain proprietary to Rithmic, LLC and must be obtained directly through your own Rithmic developer relationship. If you build from source, place the `RProtocolAPI` distribution at `proto/source/` and run `just proto` (the build hook does this automatically during `uv build`).

### Takedown contact

If you are an authorized representative of Rithmic, LLC or Trading Technologies International and object to the distribution of compiled protocol bindings derived from your `.proto` definitions, please open an issue at <https://github.com/alexandre-meline/pyrithmic/issues> or contact the maintainer directly. Such requests will be honored promptly with PyPI takedown and repository archival.

### Use at your own risk

This software interfaces with a live trading protocol. Bugs can lead to incorrect order routing, missed fills, or unintended position state. Always test extensively against a paper account before deploying to live trading. The authors and contributors assume no liability for financial losses arising from the use of this software.
