Metadata-Version: 2.1
Name: primary-api
Version: 0.3.0
Summary: Primary API Library
Author-email: Chi Tudi <chitudi54@gmail.com>
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pydantic ==2.13.4
Requires-Dist: requests ==2.34.2
Requires-Dist: simplejson ==4.1.1
Requires-Dist: websocket-client ==1.9.0

# primary-api

Python library for connecting to the **Primary API** (Matba Rofex exchange). Fork/refactor of `pyRofex`. Provides REST and WebSocket clients for market data, order management, and account reports.

**Stack:** Python 3.8+, `requests`, `simplejson`, `websocket-client`, `pydantic`

---

## Installation

```bash
pip install primary-api
```


## Quick Start

```python
from primary_api import PrimaryApiClient
from primary_api.models.enums import MarketDataEntry, MarketId
from primary_api.models.market_data import MarketDataMessage, MarketDataResponse

client = PrimaryApiClient(
    username="username",
    password="password",
    account="account123",
    rest_url="https://api.remarkets.primary.com.ar/",
    ws_url="wss://api.remarkets.primary.com.ar/"
)

# REST — returns typed Pydantic models
segments = client.get_segments()
print(segments.segments[0].market_segment_id)

md: MarketDataResponse = client.get_market_data("DLR/MAY26", [MarketDataEntry.LAST])
print(md.market_data.LA.price)

# WebSocket — handlers receive typed models
def on_md(msg: MarketDataMessage):
    print(msg.market_data.LA.price)

sub = client.market_data_subscription(
    tickers=["DLR/MAY26"],
    entries=[MarketDataEntry.BIDS, MarketDataEntry.OFFERS],
    handler=on_md
)

client.wait_for_interrupt()
```

## Models

All API responses are typed Pydantic models with automatic camelCase to snake_case conversion. Every `get_*` method returns a typed model (not a raw dict).

```python
segments: SegmentsResponse = client.get_segments()
print(segments.segments[0].market_segment_id)
```

Key models:

| Model | Description |
|---|---|
| `SegmentsResponse` | Available market segments |
| `AllInstrumentsResponse` | All instruments with CFI codes |
| `InstrumentsByResponse` | Instruments by CFI code or segment |
| `DetailedInstrumentsResponse` | Full instrument details |
| `InstrumentDetailsResponse` | Single instrument details |
| `MarketDataResponse` | Bids, offers, last price, etc. |
| `OrderStatusResponse` | Order status / list of orders |
| `OrderConfirmationResponse` | Order send/cancel confirmation |
| `PositionsResponse` | Account positions |
| `DetailedPositionsResponse` | Detailed positions by contract type |
| `AccountReportResponse` | Account balance and report |
| `TradesResponse` | Trade history |
| `MarketDataMessage` | Parsed WS market data message (has `instrument_id` + `market_data`) |
| `OrderReportMessage` | Parsed WS order report message (has `order_report`) |
| `EnhancedOrderReport` | OrderReport with helper methods (`is_open()`, `is_closed()`, etc.) |

```bash
pip install primary-api-models
```

For docs, visit https://pypi.org/project/primary-api-models/


## Configuration

### Environment

The client auto-detects the environment from the URL. If `remarkets` is in the URL, it configures `is_remarkets = True` and uses `ISV_PBCP` as the proprietary value.

| Endpoint | URL |
|---|---|
| Remarkets (testing) | `https://api.remarkets.primary.com.ar/` |
| Live production | Provided by your broker |

### Production environment

```python
from primary_api import PrimaryApiClient

client = PrimaryApiClient(
    username="matriz-username",
    password="matriz-password",
    account="matriz-account",
    rest_url="https://api.your-broker.xoms.com.ar",
    ws_url="wss://api.your-broker.xoms.com.ar"
)
```

## REST API

### Instruments

| Method | Return Type | Description |
|---|---|---|
| `get_segments()` | `SegmentsResponse` | Get all available market segments |
| `get_all_instruments()` | `AllInstrumentsResponse` | Get all instruments |
| `get_detailed_instruments()` | `DetailedInstrumentsResponse` | Get detailed instrument list |
| `get_instrument_details(ticker, market)` | `InstrumentDetailsResponse` | Get details for a specific instrument |
| `get_instruments_by_cfi(cfi_code)` | `InstrumentsByResponse` | Filter instruments by `CFICode` |
| `get_instruments_by_segment(market_segment, market)` | `InstrumentsByResponse` | Filter instruments by `MarketSegment` |


### Market Data

#### Entries
| Símbolo | Significado | Descripción |
|----------|-------------|-------------|
| BI | BIDS | Mejor oferta de compra en el Book |
| OF | OFFERS | Mejor oferta de venta en el Book |
| LA | LAST | Último precio operado en el mercado |
| OP | OPENING PRICE | Precio de apertura |
| CL | CLOSING PRICE | Precio de cierre de la rueda de negociación anterior |
| SE | SETTLEMENT PRICE | Precio de ajuste (solo para futuros) |
| HI | TRADING SESSION HIGH PRICE | Precio máximo de la rueda |
| LO | TRADING SESSION LOW PRICE | Precio mínimo de la rueda |
| TV | TRADE VOLUME | Volumen operado en contratos/nominales para ese security |
| OI | OPEN INTEREST | Interés abierto (solo para futuros) |
| IV | INDEX VALUE | Valor del índice (solo para índices) |
| EV | TRADE EFFECTIVE VOLUME | Volumen efectivo de negociación para ese security |
| NV | NOMINAL VOLUME | Volumen nominal de negociación para ese security |
| ACP | AUCTION PRICE | Precio de cierre del día corriente |

| Method | Return Type | Description |
|---|---|---|
| `get_market_data(ticker, entries, depth, market)` | `MarketDataResponse` | Get market data (bids, offers, last price, etc.) |

### Orders

| Method | Return Type | Description |
|---|---|---|
| `send_order(ticker, size, side, ...)` | `OrderConfirmationResponse` | Send a new order (REST) |
| `send_order_via_websocket(...)` | `None` | Send a new order (WebSocket) |
| `cancel_order(client_order_id, proprietary)` | `OrderConfirmationResponse` | Cancel an existing order (REST) |
| `cancel_order_via_websocket(...)` | `None` | Cancel an existing order (WebSocket) |
| `get_order_status(client_order_id, proprietary)` | `OrderStatusResponse` | Check order status by client ID |
| `get_all_orders_by_id(client_order_id, proprietary)` | `OrderStatusResponse` | All orders matching a client ID |
| `get_order_by_order_id(order_id, proprietary)` | `OrderStatusResponse` | Order by server-assigned order ID |
| `get_order_by_exec_id(exec_id, proprietary)` | `OrderStatusResponse` | Order by execution ID |
| `get_all_orders_status(account)` | `OrderStatusResponse` | Get all orders for an account |
| `get_active_orders(account)` | `OrderStatusResponse` | Get active (open) orders |
| `get_filled_orders(account)` | `OrderStatusResponse` | Get filled orders |

### Account

| Method | Return Type | Description |
|---|---|---|
| `get_account_position(account)` | `PositionsResponse` | Get account position |
| `get_detailed_position(account)` | `DetailedPositionsResponse` | Get detailed position by contract type |
| `get_account_report(account)` | `AccountReportResponse` | Get account report (balance, margins) |
| `get_trade_history(ticker, date, date_from, date_to, market)` | `TradesResponse` | Get trade history |

### Example: Sending an Order

```python
from primary_api.models.enums import Side, OrderType, TimeInForce, MarketId

order = client.send_order(
    ticker="DLR/MAY26",
    size=10,
    order_type=OrderType.LIMIT,
    side=Side.BUY,
    market=MarketId.ROFX,
    time_in_force=TimeInForce.DAY,
    price=850.50,
)
print(order.order.client_id)
```

## WebSocket Subscriptions

### init_websocket_connection

Set up all handlers and establish the WebSocket connection in one call (pyRofex compatible):

```python
from primary_api.models.market_data import MarketDataMessage
from primary_api.models.order_report import OrderReportMessage

def on_market_data(msg: MarketDataMessage):
    print(msg.market_data.LA)

def on_order_report(msg: OrderReportMessage):
    print(msg.order_report.cl_ord_id)

def on_error(msg: dict):
    print(f"Error: {msg}")

def on_exception(e: Exception):
    print(f"Exception: {e}")

client.init_websocket_connection(
    market_data_handler=on_market_data,
    order_report_handler=on_order_report,
    error_handler=on_error,
    exception_handler=on_exception,
)

# Handlers are already set — subscribe without passing handler
sub = client.market_data_subscription(tickers=["DLR/MAY26"], entries=[...])
```

### Market Data Subscription

```python
from primary_api.models.market_data import MarketDataMessage

def handle_market_data(msg: MarketDataMessage):
    print(msg.market_data.LA.price)

sub = client.market_data_subscription(
    tickers=["DLR/MAY26", "DLR/JUN26"],
    entries=[
        MarketDataEntry.BIDS,
        MarketDataEntry.OFFERS,
        MarketDataEntry.LAST,
    ],
    depth=5,
    handler=handle_market_data
)

print(sub.count())       # 2 tickers
print(sub.to_dict())     # full details
```

### Order Report Subscription

```python
from primary_api.models.order_report import OrderReportMessage

def handle_order_report(msg: OrderReportMessage):
    print(msg.order_report.cl_ord_id)

sub = client.order_report_subscription(
    handler=handle_order_report
)

client.unsubscribe_order_report()
```

### WebSocket Lifecycle

- **First `connect()`:** raises `ApiException` if the endpoint is unreachable.
- **Auto-reconnect:** after a successful connection, if the WebSocket drops, it reconnects automatically in a background thread. All active subscriptions are re-sent on reconnect.
- **Parsed messages:** incoming WS messages are parsed into typed Pydantic models before reaching handlers — `MarketDataMessage` for market data, `OrderReportMessage` for order reports.
- **Cleanup:** call `client.close_websocket()` to stop the reconnection loop.
- **Unsubscribe:** use `client.unsubscribe_market_data()` / `client.unsubscribe_order_report()`.

## Enums Reference

All enums in `primary_api.models.enums`:

| Enum | Values |
|---|---|
| `StatusResponse` | `OK`, `ERROR` |
| `MessageType` | `MARKET_DATA`, `ORDER_REPORT` |
| `MarketId` | `ROFX` |
| `Side` | `BUY`, `SELL` |
| `OrderType` | `LIMIT`, `MARKET`, `STOP_LIMIT`, `STOP_LIMIT_MERVAL`, `MARKET_TO_LIMIT`, `PREVIOUSLY_QUOTED`, `STOP` |
| `OrderStatus` | `PENDING_NEW`, `PENDING_CANCEL`, `PENDING_REPLACE`, `NEW`, `FILLED`, `PARTIALLY_FILLED`, `REPLACED`, `CANCELED`, `CANCELLED`, `EXPIRED`, `REJECTED` |
| `TimeInForce` | `DAY`, `IOC`, `FOK`, `GTD`, `GTC` |
| `Currency` | `ARS`, `USD`, `EXT`, `USG`, `UYD`, `UYU` |
| `CurrencyIndex` | `ARS`, `ARS BCRA`, `EUR`, `UYU`, `U$S`, `USD MtR`, `USD UY`, `USD DB`, `USD G`, `USD R`, `USD C`, `USD D` |
| `SettlType` | `T0` (CI), `T1` (24hs), `T2` (48hs), `T3` (72hs), `T4` (96hs), `T5` (120hs) |
| `SettlementIndex` | `T0`–`T5` (int 0–5) |
| `MarketDataEntry` | `BIDS`, `OFFERS`, `LAST`, `OPENING_PRICE`, `CLOSING_PRICE`, `SETTLEMENT_PRICE`, `HIGH_PRICE`, `LOW_PRICE`, `TRADE_VOLUME`, `OPEN_INTEREST`, `INDEX_VALUE`, `TRADE_EFFECTIVE_VOLUME`, `NOMINAL_VOLUME`, `ACP`, `TRADE_COUNT` |
| `CFICode` | `STOCK`, `BOND`, `FUTURE`, `PUT_STOCK`, `CALL_STOCK`, `PUT_FUTURE`, `CALL_FUTURE`, `CEDEAR`, `REPO`, etc. |
| `MarketSegment` | `MERV`, `MFCI`, `DDF`, `DDA`, `DUAL`, `MAE`, `MATBA`, `TIVA`, `U-DDF`, `U-DDA`, `U-DUAL`, `U-DDF-S`, `U-FIN`, `U-COMM`, `U-STOCK`, `MVR`, `MVC`, `MVM`, `AVS`, `TEST` |
| `ContractType` | `BOND`, `CEDEAR`, `FUTURE`, `NEGOTIABLE_OBLIGATION`, `OPTION_PUT`, `OPTION_CALL`, `STOCK`, `LETTER_NOTE`, `DEFAULT`, `REPURCHASE` |


---

## Tests development

### Environment variables (for testing)

Create a `.env` file:

```
CL_USERNAME=your_user
CL_PASSWORD=your_pass
CL_ACCOUNT=your_account
CL_API_HTTPS=https://api.remarkets.primary.com.ar/
CL_API_WSS=wss://api.remarkets.primary.com.ar/
```
