Metadata-Version: 2.4
Name: coindubo
Version: 0.1.0
Summary: Python SDK for the Coindubo Prediction Market API.
Author-email: Coindubo <bot@coindubo.com>
License: MIT
Project-URL: Homepage, https://coindubo.com
Keywords: coindubo,prediction-market,trading,sdk,api
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Dynamic: license-file

# coindubo

Python SDK for the [Coindubo](https://coindubo.com) Prediction Market API.

Trade BTC binary-outcome prediction markets programmatically. Zero runtime
dependencies. Python 3.9+.

## Install

```bash
pip install coindubo
```

## Quickstart

Get an API key from your Coindubo Profile & API Keys page, then:

```python
from coindubo import Client

client = Client(api_key="sk_...")

# Wait for the next market to open and place an order on it
active = client.markets.wait_for_active()
ack = client.orders.place(
    market_id=active.market.id,
    side="UP",            # "UP" (yes) or "DOWN" (no)
    direction="BUY",      # "BUY" (open position) or "SELL" (close position)
    price=0.40,           # 0.01–0.99
    quantity=10,
)
print("placed", ack.order_id)

# Watch fills as they come in
def on_fill(order):
    print("filled:", order.id, "at avg", order.price)

client.orders.watch_fills([ack.order_id], on_fill=on_fill, deadline_s=60.0)
```

## What's in the SDK

The `Client` exposes four domain APIs and the underlying transports.

### `client.markets` — market data and the `wait_for_active` primitive

| Method | Purpose |
|---|---|
| `list(date=None, status=None)` | List markets, optionally filtered |
| `get(market_id)` | Fetch a market by id |
| `get_by_slug(slug)` | Fetch a market by slug |
| `active()` | The market currently open for trading (or `NotFoundError`) |
| `nearby(slug, before=5, after=5)` | Markets surrounding a slug |
| `orderbook(market_id)` | Snapshot of UP/DOWN bids and asks |
| `candles(start=None, end=None)` | OHLCV BTC candles |
| `price_history()` | Last ~5 minutes of BTC ticks |
| `wait_for_active(poll_interval=3.0)` | Poll until a market opens |

### `client.orders` — placement, cancellation, fill tracking

| Method | Purpose |
|---|---|
| `place(market_id, side, direction, price, quantity)` | Place one limit order |
| `place_batch(orders: List[OrderRequest])` | Place many orders in sequence |
| `list(status=None)` | List the user's orders |
| `cancel(order_id)` | Cancel an open order |
| `watch_fills(order_ids, on_fill, poll_interval=1.0, deadline_s=60.0)` | Block until orders reach a terminal state, calling `on_fill(order)` per fill |

### `client.portfolio` — wallet, positions, trades, snapshots

| Method | Purpose |
|---|---|
| `wallet()` | Balance and used margin |
| `ledger(limit=50, offset=0)` | Paginated wallet ledger |
| `positions(settled=None)` | Open or settled positions |
| `trades(limit=50, offset=0)` | Paginated trade history |
| `market_trades(market_id, limit=50, offset=0)` | Trades for one market |
| `snapshot(market_slug=None)` | Wallet + positions + open orders + recent trades + nearby markets, in a single call |

### `client.stream` — typed WebSocket subscriptions

Each method registers a callback. Returns a handle whose `.close()` unsubscribes.

| Method | Event type |
|---|---|
| `orderbook(market_id, on_message)` | `OrderbookUpdate` |
| `trades(market_id, on_message)` | `TradeEvent` |
| `prices(on_message)` | `PriceTickEvent` |
| `markets(on_message)` | `MarketEvent` |
| `positions(on_message)` | `PositionUpdate` |

The underlying WebSocket reconnects automatically with exponential backoff
(1s base, 30s cap) and re-subscribes to all active channels on reconnect.

## Errors

Every SDK failure raises a subclass of `CoinduboError`:

```python
from coindubo import (
    CoinduboError,        # base
    AuthError,            # 401 / 403
    RateLimitError,       # 429 — has .retry_after_ms
    ValidationError,      # 400
    NotFoundError,        # 404
    ServerError,          # 5xx
    WebSocketError,       # WS handshake / framing issue
    BatchPlacementError,  # OrdersAPI.place_batch partial failure
)

try:
    client.orders.place(...)
except RateLimitError as e:
    time.sleep(e.retry_after_ms / 1000.0)
except CoinduboError:
    # catch-all
    raise
```

`BatchPlacementError` carries `placed: List[OrderAck]`, `failure: Exception`,
and `failed_index: int` so callers can cancel orphaned orders if a mid-batch
order placement fails.

## Configuration

```python
client = Client(
    api_key="sk_...",
    base_url="https://api.coindubo.com/api/v1",  # default
    timeout=30.0,                                # per-request HTTP timeout in seconds
    max_retries=3,                               # for transient 429 / 5xx
    ws_url=None,                                 # override the derived WS URL
    ws_path="/ws",                               # override only the WS path
)
```

`ws_url` (when given) wins over `ws_path`. Both are useful for staging
deployments where the WebSocket lives at a non-default address.

## Examples

See the `examples/` directory in the source repo:

- `accumulate.py` — symmetric ladder on each new market
- `momentum.py` — directional orders based on BTC momentum
- `grid.py` — buy ladder + counter-sell on fills
- `orderbook_watch.py` — live orderbook stream

Each example is < 50 lines.

## Threading model

The SDK is sync-only. Concurrency is via threads:

- `client.http.*` calls are blocking.
- `client.stream.*` uses a single background daemon thread to read from the
  WebSocket. User callbacks fire on that thread; do not block in them.
- Multiple threads can call `client.http.*` concurrently — `urllib`'s opener
  is thread-safe.

## Support

Bugs / questions: bot@coindubo.com

## License

MIT
