Metadata-Version: 2.4
Name: blockfill
Version: 0.1.19
Summary: Python SDK for the blockfill execution daemon
Project-URL: Homepage, https://github.com/mavri-x/blockfill
Author-email: Mavri-X <robot@mavri-x.ai>
License: Proprietary
Requires-Python: >=3.10
Requires-Dist: toml
Description-Content-Type: text/markdown

# blockfill Python SDK

Python wrapper for the [blockfill](../executor/) execution daemon. Manage exchange credentials, run the daemon, and operate tickets (place / query / cancel) over a local UDS RPC socket.

## Requirements

- Python 3.10+
- **linux-x86_64** — the daemon binary is bundled inside the wheel. PyPI publishes a `manylinux2014_x86_64`-tagged wheel only; macOS / Windows / arm64 hosts will be cleanly rejected by `pip` with "no matching distribution".

## Install

### As a library (`from blockfill import Blockfill`)

```bash
python3 -m venv .venv && source .venv/bin/activate
pip install blockfill
```

### As a CLI only (`blockfill ticket place ...`)

```bash
pipx install blockfill
```

(`pipx` keeps the SDK in its own isolated venv and links the `blockfill` /
`bf` CLI to `~/.local/bin/`. On Debian 12+ / Ubuntu 23+, `pipx` is the
recommended way to install Python CLI apps without hitting PEP 668's
"externally-managed-environment" error.)

### Upgrading

`pip` will **not** auto-upgrade an already-installed package — re-running
`pip install blockfill` is a no-op if any version is already there. To pull
the latest release:

```bash
pip install -U blockfill           # latest
pip install blockfill==0.1.4       # pin specific version
pipx upgrade blockfill             # if installed via pipx
```

Check the live version on PyPI:

```bash
pip index versions blockfill
# or
curl -s https://pypi.org/pypi/blockfill/json | python3 -c \
    "import sys,json;print(json.load(sys.stdin)['info']['version'])"
```

### What you get

The wheel ships with the executor binary pre-built. No GitHub release, no
`install()` call — `pip install` is everything. The qtex endpoint and API
key are also hardcoded into the binary; users never set them.

## Verify the install

One-liner sanity check before anything else:

```python
from blockfill import Blockfill
print(Blockfill().version())   # → "blockfill 0.1.4"
```

If that prints a version, everything is wired up correctly. If it errors:

| Error                                              | Meaning                                                                |
| -------------------------------------------------- | ---------------------------------------------------------------------- |
| `ModuleNotFoundError: No module named 'blockfill'` | wrong Python env (forgot `source .venv/bin/activate`?)                 |
| `BinaryNotFound: ... _bin/blockfill`               | wheel missing the bundled binary (re-install with `--force-reinstall`) |
| `OSError: [Errno 8] Exec format error`             | wrong-platform wheel installed (shouldn't happen on a fresh install)   |
| `GLIBC_X.X not found`                              | host glibc older than `manylinux2014` (= glibc 2.17)                   |

## Quickstart

> ⚠️ **Order matters.** `bf.start()` will refuse to launch if no exchange
> credentials are configured. Always call `set_credentials()` first on a
> fresh machine — see `DaemonStartTimeout: no config.toml at ...` below.

```python
from blockfill import Blockfill

bf = Blockfill(data_dir="~/.blockfill")

# 1) Write exchange credentials to ~/.blockfill/config.toml (chmod 0600).
#    SDK auto-runs `check_credentials` (a signed REST round-trip) — proves
#    auth works AND the host can reach the exchange. If you're behind a
#    geo block (US → binance), set a proxy first via bf.set_proxy(...).
bf.set_credentials("binance-futures", api_key="...", api_secret="...", testnet=True)

# 2) Start daemon. ~50s warmup while it fetches market data.
bf.start()
bf.health()  # returns DaemonStatus(running=False, ...) if anything is wrong

# Place a ticket
ticket = bf.place(
    exchange="binance-futures",
    symbol="btcusdt",
    strategy="maker",
    target_position=0.1,
    time_constraint_ms=300_000,
)
print(ticket.ticket_id, ticket.status)  # tkt_xxx NEW

# Query active session (in-memory)
tickets = bf.query(status="NEW")

# Query persistent history (qtex MongoDB; survives daemon restarts)
historical = bf.query(status="COMPLETE", history=True)

# Cancel
bf.cancel(ticket.ticket_id)

# Shut down daemon
bf.stop()
```

---

## API Reference

### `Blockfill(data_dir, binary_path, timeout_s)`

| Parameter     | Default                  | Description                                        |
| ------------- | ------------------------ | -------------------------------------------------- |
| `data_dir`    | `~/.blockfill`           | Data directory (config, socket, logs, trading log) |
| `binary_path` | bundled `_bin/blockfill` | Override the bundled binary path (rarely needed)   |
| `timeout_s`   | `10.0`                   | Default RPC call timeout (seconds)                 |

---

### Version

```python
bf.version() -> str
# Returns "blockfill 0.1.X"
```

---

### Credentials

```python
bf.set_credentials(
    exchange: str,            # "binance-futures" | "okx-swap"
    api_key: str,
    api_secret: str,
    api_passphrase: str | None = None,  # OKX only
    testnet: bool = False,
) -> None
# Writes the [exchanges.<name>] block of {data_dir}/config.toml (chmod 0600)
```

The qtex endpoint and API key are **compiled into the binary** at release time — you do not configure them.

---

### Daemon

```python
bf.start(wait_timeout_s=10.0, env=None) -> None
# Spawns the daemon in the background, returns once the UDS socket is bound.
# Idempotent — no-op if already running.

bf.stop(wait_timeout_s=5.0) -> None
# Graceful shutdown.

bf.restart() -> None

bf.health() -> DaemonStatus
# Always returns a DaemonStatus (`running=False, ...` when the daemon is
# not reachable — never raises). Single entry point for everything you
# need to know about the daemon's state.
```

For debugging, tail the daemon log directly:

```bash
tail -F ~/.blockfill/runtime/daemon.startup.log
```

`DaemonStatus` fields:

| Field             | Type          | Meaning                                              |
| ----------------- | ------------- | ---------------------------------------------------- |
| `running`         | `bool`        | Daemon process is up and the RPC socket answered.    |
| `pid`             | `int`         | Daemon process id (0 if not running).                |
| `exchanges`       | `list[str]`   | Configured exchanges (from `config.toml`).           |
| `ready_exchanges` | `list[str]`   | Exchanges whose executor finished warmup.            |
| `active_tickets`  | `int`         | NEW/OPEN tickets currently tracked.                  |
| `uptime_s`        | `int`         | Daemon process uptime in seconds.                    |
| `version`         | `str`         | Daemon binary version (e.g. `"0.1.14"`).             |
| `proxy`           | `str \| None` | Active outbound proxy URL (or None for direct).      |
| `qtex`            | `bool`        | True if qtex is reachable. qtex hosts the ticket-history endpoints (`/public/v1/tickets/*`) used by `bf.query(history=True)`; when down, live trading is unaffected but history queries fail. Updated by a 10s background ping; flips False after 30s of no response. |
| `exchange` (prop) | `dict[str, ExchangeStatus]` | Per-exchange status — see below.       |

Per-exchange readiness (the only way to check ready — there is no
top-level `ready` flag because "all-ready" is rarely meaningful in a
multi-exchange daemon):

```python
s = bf.status()
s.exchange
# → {
#     "binance-futures": ExchangeStatus(ready=True),
#     "okx-swap":        ExchangeStatus(ready=False),
#   }

if s.exchange["binance-futures"].ready:
    bf.place(exchange="binance-futures", ...)
```

`ready=True` means the executor finished warmup (kline fetch, market
state, api module — `ready_cb` fired). `ready=False` covers three cases:
daemon not running, warmup in progress, or init failing in retry loop.

---

### Tickets

#### Strategies

| `strategy` | Behavior                                                                                                                                                                                                                                                          |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"maker"`  | **Passive maker.** Posts PostOnly limit orders that sit on the book. In the last segment of the time window, falls back to IOC to clean up any unfilled remainder. Lower fees (maker rebate when available), no guarantee of full fill if the book never crosses. |
| `"twap"`   | **Pure-taker TWAP.** Places IOC orders on a TWAP schedule across the time window — no PostOnly phase. Guarantees completion at the cost of crossing the spread on every slice.                                                                                    |

```python
bf.place(
    exchange: str,
    symbol: str,
    strategy: str = "maker",        # "maker" | "twap" (see table above)
    target_position: float,          # positive = long, negative = short
    time_constraint_ms: int = 300_000,  # 10_000 .. 86_400_000
) -> Ticket

bf.query(
    status: str | None = None,       # "NEW" | "OPEN" | "COMPLETE" | "CANCEL"
    symbol: str | None = None,
    ticket_id: str | None = None,
    from_ms: int | None = None,
    to_ms: int | None = None,
    limit: int = 100,
    history: bool = False,           # False=in-memory; True=qtex MongoDB
) -> list[Ticket]

bf.cancel(ticket_id=None, symbol=None, all=False) -> None | int
# - cancel(ticket_id="tkt_...") -> None     # raises TicketNotFound if missing
# - cancel(symbol="btcusdt")    -> int      # cancel NEW+OPEN for that symbol
# - cancel(all=True)            -> int      # cancel everything active

bf.cancel_all() -> int
```

`Ticket` fields:

| Field                 | Type            | Notes                                                   |
| --------------------- | --------------- | ------------------------------------------------------- |
| `ticket_id`           | `str`           | `tkt_<hex>`                                             |
| `status`              | `str`           | `NEW` / `OPEN` / `COMPLETE` / `CANCEL`                  |
| `exchange`            | `str`           | `binance-futures` / `okx-swap`                          |
| `symbol`              | `str`           | exchange-format symbol                                  |
| `strategy`            | `str`           | `maker` / `twap`                                        |
| `target_position`     | `float`         | requested net position                                  |
| `init_position`       | `float \| None` | exchange position at activation time                    |
| `executed_position`   | `float \| None` | actual delta filled so far                              |
| `time_constraint_ms`  | `int`           | execution time limit                                    |
| `start_time_ms`       | `int \| None`   | set when executor activates the ticket (NEW → OPEN)     |
| `last_update_time_ms` | `int \| None`   | refreshed on every state change                         |
| `is_expired`          | `bool`          | flag-only; status stays OPEN until separately cancelled |
| `cancel_reason`       | `str \| None`   | see table below                                         |

**`cancel_reason` values**: `external`, `superseded`, `stale`, `rejected`, `min_notional`, `risk_breach`, `insufficient_margin`, `paused`

**Auto-supersede**: placing a new ticket for the same `exchange+symbol` immediately cancels any existing `NEW`/`OPEN` ticket for that pair (`cancel_reason="superseded"`). The superseded ticket remains in query results.

---

### Diagnostics

```python
bf.check_credentials() -> int
# Call a SIGNED endpoint on each configured exchange to verify api_key+secret
# really authenticate. Exit code 0 = all pass. Detects:
# - wrong key/secret
# - IP whitelist mismatch (your outbound IP not on Binance whitelist)
# - testnet/mainnet flag wrong
# - network / proxy / geo block (signed REST round-trip implies reachability)
# Auto-invoked at the end of `set_credentials(...)` / `blockfill
# set-credentials` so typos surface immediately.
```

### Positions

```python
bf.positions() -> list[dict]
# Each entry: {exchange, symbol, size, entry_price, update_ts_ms}
# Aggregated across all running executors.
```

### Proxy / Geo-bypass (for Starchild AI, US-IP users, etc.)

```python
bf.set_proxy("http://jp:x@sc-vpn.internal:8080")  # Starchild sc-vpn skill
# Or any HTTP CONNECT proxy URL:
bf.set_proxy("http://user:pass@proxy.example.com:8080")
bf.set_proxy(None)                                  # remove

bf.start()  # daemon picks up the proxy from config.toml
```

The proxy applies to **all REST traffic** from daemon → exchange.
WebSocket proxy is on the TODO list — until then, daemon's market-data
streams go direct (and will fail on geo-blocked hosts).

Verify before paying for IPRoyal: after `bf.set_proxy(...)` set credentials —
the SDK's auto `check_credentials` will fail loudly if the proxy can't reach
the exchange, so you know before you start the daemon.

---

### Context Manager

```python
with Blockfill() as bf:
    bf.start()
    ticket = bf.place(...)
# daemon is stopped on exit
```

---

### Exceptions

| Exception                 | When                                                               |
| ------------------------- | ------------------------------------------------------------------ |
| `BinaryNotFound`          | bundled binary missing — `pip install --force-reinstall blockfill` |
| `DaemonNotRunning`        | daemon socket not found or unreachable                             |
| `DaemonStartTimeout`      | `start()` timed out waiting for the daemon                         |
| `RpcError(code, message)` | daemon returned a JSON-RPC error                                   |
| `TicketNotFound`          | `cancel(ticket_id=...)` called with non-existent ticket            |
| `CredentialsError`        | invalid exchange name in `set_credentials()`                       |
| `InvalidApiKey`           | qtex rejected the embedded BLOCKFILL_API_KEY (HTTP 401)            |

---

## Patterns

### Strategy system integration

```python
from blockfill import Blockfill

bf = Blockfill()

if not bf.health().running:
    bf.start()

# On each signal
ticket = bf.place(
    exchange="binance-futures",
    symbol=symbol,
    strategy="maker",
    target_position=position,
    time_constraint_ms=300_000,
)
```

### Check open positions

```python
open_tickets = bf.query(status="NEW") + bf.query(status="OPEN")
for t in open_tickets:
    print(t.symbol, t.target_position, t.init_position, t.executed_position)
```

### Audit past runs

```python
# In-memory query only sees the current session.
# Use history=True to reach qtex MongoDB for COMPLETE / CANCEL from past sessions.
recent_completes = bf.query(status="COMPLETE", history=True, from_ms=ts_ms_24h_ago)
```

---

## Directory Structure

```
~/.blockfill/
├── config.toml              # exchange credentials (chmod 0600)
├── runtime/
│   ├── daemon.sock          # UDS socket (SDK ↔ daemon IPC)
│   ├── daemon.pid           # PID file
│   └── daemon.log           # daemon logs
└── trading-log/             # Channel B upload retry buffer
```

The daemon binary lives **inside** the installed package
(`site-packages/blockfill/_bin/blockfill`), not in `~/.blockfill/`.

---

## Supported Exchanges

| Exchange        | Value               |
| --------------- | ------------------- |
| Binance Futures | `"binance-futures"` |
| OKX Swap        | `"okx-swap"`        |

### ⚠️ Symbol format differs per exchange

Each exchange uses its **own native symbol format**. blockfill does NOT
cross-translate — pass the format the target exchange expects:

| Exchange          | Format                                  | Example                           |
| ----------------- | --------------------------------------- | --------------------------------- |
| `binance-futures` | Lowercase, concatenated                 | `btcusdt`, `dogeusdt`             |
| `okx-swap`        | Dash-separated, includes `-SWAP` suffix | `BTC-USDT-SWAP`, `DOGE-USDT-SWAP` |

```python
# Binance — lowercase native
bf.place(exchange="binance-futures", symbol="dogeusdt", target_position=100)

# OKX — dash + SWAP suffix native
bf.place(exchange="okx-swap", symbol="DOGE-USDT-SWAP", target_position=-100)
```

Passing the wrong format returns `cancel_reason="rejected"` immediately (no
order ever reaches the exchange).
