Metadata-Version: 2.4
Name: blockfill
Version: 0.1.31
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

Algorithmic execution for crypto perps. You declare a target position; blockfill
works the order over a time window using **maker** (PostOnly resting + IOC
fallback) or **twap** (taker-sliced) strategies, cutting execution slippage vs.
naïve market orders.

**Self-custodian.** Your exchange API keys stay on your machine (`~/.blockfill/config.toml`,
chmod 0600). The daemon signs orders locally and talks directly to the exchange —
no key custody, no order routing, no order-flow visibility by a third party. The
package only ships the execution engine.

One Python API, one local daemon, two exchanges supported today
(`binance-futures`, `okx-swap`), one `bf.place(...)` call per target position.

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

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

`-U` is intentional: it installs the latest release if you don't have it, and
upgrades in place if you do (without it, `pip install blockfill` is a no-op
when any version is already installed).

## Verify the install

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

## 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()

# 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.status()  # 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")

# Cancel
bf.cancel(ticket.ticket_id)

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

---

## API Reference

### 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),
# runs check_credentials() to print verification, then stops + starts the
# daemon (when running) so the new user_id/creds load immediately — avoids
# identity confusion where `place` / history queries would otherwise keep
# operating under the OLD creds cached in the running daemon.
```

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.status() -> 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).                                                                                                                                                                                                                                 |
| `exchange` (prop) | `dict[str, ExchangeStatus]` | Per-exchange status keyed by name — see below. **Primary way** to check what's configured and what's ready.                                                                                                                                                           |
| `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.X"`).                                                                                                                                                                                                                               |
| `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. |

`exchanges` (`list[str]`, configured names) and `ready_exchanges`
(`list[str]`, warmed-up names) are also present on the dataclass as
flat-list shortcuts — they're just `list(exchange.keys())` and
`[n for n,e in exchange.items() if e.ready]` respectively. Prefer
`exchange[name].ready` in code.

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 (REST symbol/book fetch,
authenticated `get_account_balances`, market-stream connect — `ready_cb`
fired). `ready=False` covers:

- daemon not running
- warmup in progress (typical 30–60s after `bf.start()`)
- init failed and supervisor is in its retry backoff (bad creds, IP
  whitelist, geo block, exchange API down)

`bf.place(exchange=X, ...)` while `X` is not ready is rejected at the RPC
layer with `-32000 X not ready — executor still warming up; poll
system.status until ready_exchanges contains it` — no ticket is created.

---

### 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,  # 60_000 .. 86_400_000 (1min .. 24h)
) -> 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() -> None
# Calls a SIGNED REST endpoint on each configured exchange and prints one
# line per exchange:
#   ✓ binance-futures       3 asset balances returned
#   ✗ okx-swap              API error 50101: APIKey does not match current environment.
# Detects: wrong key/secret, IP whitelist mismatch, testnet/mainnet flag
# wrong, network / proxy / geo block. Does not raise, does not return a
# status code — visual output is the signal.
# Auto-invoked at the end of `set_credentials(...)`.
```

### Positions

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

### Proxy / Geo-bypass

```python
bf.set_proxy("http://user:pass@jp-proxy.example.com:8080")  # route via Japan
bf.set_proxy()                                              # remove
```

`set_proxy` auto-restarts the daemon (when running) so the new proxy
takes effect immediately — the daemon reads the proxy only at startup
and stashes it in a global, so a write-without-restart would leave the
running daemon on the OLD proxy.

**Starchild users (trading `binance-futures` from a US-blocked region):**
the `byo-proxy` skill provisions a non-US egress proxy URL you can paste
into `set_proxy(...)`. See
[Starchild byo-proxy SKILL.md](https://github.com/Starchild-ai-agent/official-skills/blob/main/byo-proxy/SKILL.md)
for the supported providers and how to obtain the URL.

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 committing: after `bf.set_proxy(...)` re-set credentials —
`set_credentials` auto-runs `check_credentials` which does a signed REST
round-trip through the proxy. A failure there means the proxy can't reach
the exchange, so you find out before starting the daemon.

---

### Context Manager

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

---

## Patterns

### Strategy system integration

```python
from blockfill import Blockfill

bf = Blockfill()

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

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

---

## Supported Exchanges

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

### ⚠️ Symbol format differs per exchange

Each exchange has its own native symbol format. blockfill does NOT
cross-translate — `bf.place(symbol=...)` must match exactly what the
exchange uses:

| Exchange          | Format                         | Example          |
| ----------------- | ------------------------------ | ---------------- |
| `binance-futures` | lowercase, concatenated        | `dogeusdt`       |
| `okx-swap`        | dash-separated, `-SWAP` suffix | `DOGE-USDT-SWAP` |

Don't guess — look it up with `bf.instruments(<substring>)`. It scans
all configured exchanges and returns native-format matches:

```python
bf.instruments("doge")
# [
#   {"exchange": "binance-futures", "symbol": "dogeusdt",        ...},
#   {"exchange": "okx-swap",        "symbol": "DOGE-USDT-SWAP",  ...},
#   ...
# ]

bf.place(exchange="binance-futures", symbol="dogeusdt",       target_position=100)
bf.place(exchange="okx-swap",        symbol="DOGE-USDT-SWAP", target_position=-100)
```

Wrong format → ticket auto-cancelled with `cancel_reason="rejected"`; no
order ever reaches the exchange.
