Metadata-Version: 2.4
Name: alfax
Version: 0.1.0
Summary: Python client for the Alfax Trade API (Binance-compatible).
Project-URL: Homepage, https://alfax.trade
Project-URL: Documentation, https://docs.alfax.trade
Project-URL: Repository, https://gitlab.com/alfax-group/prop
Author: Alfax Group
License: MIT
Requires-Python: >=3.12
Requires-Dist: httpx>=0.28
Requires-Dist: pydantic>=2.7
Requires-Dist: websockets>=13
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pydantic>=2.7; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Provides-Extra: examples
Requires-Dist: mypy>=1.10; extra == 'examples'
Requires-Dist: pydantic>=2.7; extra == 'examples'
Requires-Dist: pytest-asyncio>=0.23; extra == 'examples'
Requires-Dist: pytest>=8; extra == 'examples'
Requires-Dist: respx>=0.21; extra == 'examples'
Requires-Dist: ruff>=0.4; extra == 'examples'
Description-Content-Type: text/markdown

# alfax Python SDK

First-party Python 3.12+ client for the [Alfax Trade API](https://alfax.trade).

The endpoint paths, request/response schemas, authentication, and WebSocket stream
formats are **byte-compatible with Binance Futures USDT-M**. Existing `ccxt`,
`python-binance`, Freqtrade, and Hummingbot strategies work by changing only the
`base_url`.

## Install

```bash
pip install alfax
# with dev/test tools:
pip install "alfax[dev]"
```

## Quick start

```python
import os
from alfax import Client

c = Client(
    api_key=os.environ["ALFAX_KEY"],
    api_secret=os.environ["ALFAX_SECRET"],
    base_url="https://api.alfax.trade",   # default
)

# Public endpoints (no auth required)
print(c.ping())                          # {}
print(c.server_time())                   # 1700000000000
info = c.exchange_info()
print(info.symbols[0].symbol)            # "BTCUSDT"

# Private endpoints
acc = c.account()
print(acc.totalWalletBalance)            # Decimal("10000.00")
print(acc.challenge.stage)              # "STAGE_1"

# Place an order (money values are strings — no floats)
order = c.new_order(
    symbol="BTCUSDT",
    side="BUY",
    type="LIMIT",
    quantity="0.001",
    price="60000.00",
    time_in_force="GTC",
)
print(order.orderId, order.status)

# Cancel it
cancelled = c.cancel_order(symbol="BTCUSDT", order_id=order.orderId)
```

## WebSocket streams (async)

```python
import asyncio
from alfax import Client, MarketStream, UserStream

async def main():
    # Public market stream
    async with MarketStream(["btcusdt@bookTicker"], base_url="wss://api.alfax.trade") as stream:
        async for msg in stream:
            print(msg)
            break

    # Private user-data stream
    c = Client(api_key="K", api_secret="S")
    listen_key = c.new_listen_key()
    async with UserStream(listen_key=listen_key, base_url="wss://api.alfax.trade") as stream:
        async for event in stream:
            print(event)
            break

asyncio.run(main())
```

## Environment variables

| Variable         | Description                                       | Default                       |
|------------------|---------------------------------------------------|-------------------------------|
| `ALFAX_KEY`      | API key                                           | —                             |
| `ALFAX_SECRET`   | API secret                                        | —                             |
| `ALFAX_BASE_URL` | REST base URL                                     | `https://api.alfax.trade`     |

## Endpoints covered

### Public (no auth)
| Method | Path | SDK method |
|--------|------|------------|
| GET | `/fapi/v1/ping` | `client.ping()` |
| GET | `/fapi/v1/time` | `client.server_time()` |
| GET | `/fapi/v1/exchangeInfo` | `client.exchange_info()` |
| GET | `/fapi/v1/ticker/bookTicker` | `client.book_ticker()` |
| GET | `/fapi/v1/klines` | `client.klines()` |

### Private (HMAC-SHA256 signed)
| Method | Path | SDK method |
|--------|------|------------|
| GET | `/fapi/v2/account` | `client.account()` |
| GET | `/fapi/v2/positionRisk` | `client.position_risk()` |
| GET | `/fapi/v1/userTrades` | `client.user_trades()` |
| POST | `/fapi/v1/order` | `client.new_order()` |
| DELETE | `/fapi/v1/order` | `client.cancel_order()` |
| GET | `/fapi/v1/order` | `client.get_order()` |
| GET | `/fapi/v1/openOrders` | `client.open_orders()` |
| GET | `/fapi/v1/allOrders` | `client.all_orders()` |
| POST | `/fapi/v1/leverage` | `client.change_leverage()` |
| POST | `/fapi/v1/listenKey` | `client.new_listen_key()` |
| PUT | `/fapi/v1/listenKey` | `client.keepalive_listen_key()` |
| DELETE | `/fapi/v1/listenKey` | `client.close_listen_key()` |

### Alfax extensions
| Method | Path | SDK method |
|--------|------|------------|
| GET | `/fapi/v1/challenge` | `client.challenge_status()` |
| GET | `/fapi/v1/challenge/rules` | `client.challenge_rules()` |
| POST | `/fapi/v1/challenge/purchase` | `client.create_challenge_purchase(product_id=…)` |
| GET | `/fapi/v1/challenge/lifecycle` | `client.challenge_lifecycle(challenge_id)` |
| POST | `/fapi/v1/payout` | `client.request_payout(challenge_id=…, amount=…)` |
| GET | `/fapi/v1/payout` | `client.get_payout(payout_id)` |
| POST | `/fapi/v1/payout/approve` | `client.approve_payout(payout_id)` (operator) |
| POST | `/fapi/v1/payout/reject` | `client.reject_payout(payout_id, reason)` (operator) |

### Challenge purchase saga

```python
# Trader buys a challenge — a Temporal workflow on the server side
# creates the payment intent, waits for the provider webhook, verifies,
# and mints the challenge.
purchase = c.create_challenge_purchase(product_id="challenge-10k-v1")
print(purchase.workflowId, purchase.status)  # purchase-…, "initiated"

# Later (once the workflow has completed) — inspect the minted
# challenge's lifecycle workflow:
state = c.challenge_lifecycle(challenge_id)
print(state.Stage, state.Status)  # STAGE_1, ACTIVE
```

### Payouts

```python
# Trader requests a withdrawal from a FUNDED challenge
p = c.request_payout(challenge_id="44444444-…", amount="500")
print(p.payoutId, p.status)  # ?, "requested"

# Operator (key must hold "operator" permission)
c.approve_payout(p.payoutId)

# Poll until terminal
state = c.get_payout(p.payoutId)
print(state.Status, state.TxRef)   # paid, stub-tx-…

# Over-cap requests are accepted but the workflow rejects them
over = c.request_payout(challenge_id="44444444-…", amount="999999")
c.approve_payout(over.payoutId)  # signals approve; cap check still runs first
state = c.get_payout(over.payoutId)
assert state.Status == "rejected"
assert "exceeds available" in state.FailReason
```

## Error handling

```python
from alfax.errors import AlfaxError, AlfaxAPIError, AuthError, RateLimitError, OrderError

try:
    order = c.new_order(...)
except OrderError as e:
    print(f"Order rejected (code {e.code}): {e.msg}")
except RateLimitError:
    print("Rate limited — back off")
except AlfaxAPIError as e:
    print(f"API error {e.status}: {e.msg}")
```

## Examples

See [`examples/`](examples/) for runnable scripts:

- [`01_ping.py`](examples/01_ping.py) — connectivity and exchange info
- [`02_account_balance.py`](examples/02_account_balance.py) — account balances and positions
- [`03_place_limit_order.py`](examples/03_place_limit_order.py) — place and cancel a limit order
- [`04_stream_book_ticker.py`](examples/04_stream_book_ticker.py) — async WebSocket book ticker stream
- [`05_challenge_status.py`](examples/05_challenge_status.py) — challenge status and rules

## Development

```bash
python -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'
pytest -q
ruff check .
```

## Money handling

All money fields use `decimal.Decimal` — never `float`. Pass prices and
quantities as strings:

```python
# Correct
c.new_order(symbol="BTCUSDT", side="BUY", type="LIMIT",
            quantity="0.001", price="60000.00")

# Wrong — will break precision
c.new_order(symbol="BTCUSDT", side="BUY", type="LIMIT",
            quantity=0.001, price=60000.00)
```
