Metadata-Version: 2.4
Name: blockfill
Version: 0.1.11
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) One-time: write exchange credentials to ~/.blockfill/config.toml
#    (must run before bf.start() on a fresh machine)
bf.set_credentials("binance-futures", api_key="...", api_secret="...", testnet=True)

# 2) (Optional) Pre-flight: confirm the host can actually reach binance.
#    Returns structured JSON. If a US-IP / region-blocked host hits 451,
#    set a proxy before starting (see "Proxy / Geo-bypass" below).
result = bf.ping()
assert result["ok"], f"ping failed: {result}"

# 3) 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.is_running() -> bool

bf.health() -> DaemonStatus
# Raises DaemonNotRunning if the socket is missing or unreachable.

bf.run_foreground(env=None) -> None
# Blocking; for debug.
```

`DaemonStatus` fields: `running`, `pid`, `exchanges`, `active_tickets`, `uptime_s`, `version`

---

### Tickets

```python
bf.place(
    exchange: str,
    symbol: str,
    strategy: str = "maker",        # "maker" | "twap"
    target_position: float,          # positive = long, negative = short
    time_constraint_ms: int = 300_000,  # 60_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`, `warmup`, `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.ping() -> dict
# Probe exchange reachability. Machine-readable JSON:
# {
#   "ok": True,
#   "proxy": "http://jp:x@sc-vpn.internal:8080" | None,
#   "results": [
#     {"exchange": "binance-futures", "url": ".../ping", "status": 200,
#      "latency_ms": 96, "ok": True},
#     ...
#   ],
# }
# Works without daemon (reads config from disk). Prefers daemon when running.

bf.check_connectivity() -> int
# Same probe as ping(), formatted for humans. Prints status + latency,
# returns shell exit code (0 = all reachable).

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
```

### 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: run `bf.ping()` after `bf.set_proxy(...)`
and confirm `ok: True`.

---

### 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.is_running():
    bf.start()
bf.health()

# 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"`        |
