Python SDK for the blockfill execution daemon
Project description
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 bypipwith "no matching distribution".
Install
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
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 callset_credentials()first on a fresh machine — seeDaemonStartTimeout: no config.toml at ...below.
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
bf.version() -> str
# Returns "blockfill 0.1.X"
Credentials
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
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:
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):
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. |
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
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
bf.positions() -> list[dict]
# Each entry: {exchange, symbol, size, entry_price, update_ts_ms}
# Aggregated across all running executors.
Proxy / Geo-bypass
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
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
with Blockfill() as bf:
bf.start()
ticket = bf.place(...)
# daemon is stopped on exit
Patterns
Strategy system integration
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:
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file blockfill-0.1.27-py3-none-manylinux2014_x86_64.whl.
File metadata
- Download URL: blockfill-0.1.27-py3-none-manylinux2014_x86_64.whl
- Upload date:
- Size: 7.7 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3a675ebf2b4bcfd7546145eba9ae2d51698ab16931ad45fbac2b28ee4a9a8672
|
|
| MD5 |
5de25774f76e41feb4300415fcf24213
|
|
| BLAKE2b-256 |
8827f1c121a88355235a82fa7440b1dc43907bc1a156c9b3f1409f6af46d5dfc
|