blockfill 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
blockfill/__init__.py ADDED
@@ -0,0 +1,24 @@
1
+ from .client import Blockfill
2
+ from .exceptions import (
3
+ BinaryNotFound,
4
+ CredentialsError,
5
+ DaemonNotRunning,
6
+ DaemonStartTimeout,
7
+ InstallError,
8
+ RpcError,
9
+ TicketNotFound,
10
+ )
11
+ from .models import DaemonStatus, Ticket
12
+
13
+ __all__ = [
14
+ "Blockfill",
15
+ "Ticket",
16
+ "DaemonStatus",
17
+ "BinaryNotFound",
18
+ "CredentialsError",
19
+ "DaemonNotRunning",
20
+ "DaemonStartTimeout",
21
+ "InstallError",
22
+ "RpcError",
23
+ "TicketNotFound",
24
+ ]
blockfill/client.py ADDED
@@ -0,0 +1,218 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+ from typing import Self
4
+
5
+ from . import config as _config
6
+ from . import daemon as _daemon
7
+ from . import installer as _installer
8
+ from . import rpc
9
+ from .exceptions import BinaryNotFound, DaemonNotRunning, TicketNotFound
10
+ from .models import DaemonStatus, Ticket
11
+
12
+
13
+ def _parse_ticket(raw: dict, defaults: dict | None = None) -> Ticket:
14
+ # Daemon may return a partial response (place returns only ticket_id/status/exchange/symbol/target_position).
15
+ # Merge in any caller-supplied defaults for fields not present in the response.
16
+ d = {**(defaults or {}), **raw}
17
+ return Ticket(
18
+ ticket_id=d["ticket_id"],
19
+ status=d["status"],
20
+ exchange=d["exchange"],
21
+ symbol=d["symbol"],
22
+ strategy=d.get("strategy", ""),
23
+ target_position=d["target_position"],
24
+ executed_position=d.get("executed_position"),
25
+ time_constraint_ms=d.get("time_constraint_ms", 0),
26
+ start_time_ms=d.get("start_time_ms"),
27
+ last_update_time_ms=d.get("last_update_time_ms"),
28
+ is_expired=d.get("is_expired", False),
29
+ cancel_reason=d.get("cancel_reason"),
30
+ )
31
+
32
+
33
+ class Blockfill:
34
+ def __init__(
35
+ self,
36
+ data_dir: str | Path = "~/.blockfill",
37
+ binary_path: str | Path | None = None,
38
+ timeout_s: float = 10.0,
39
+ ) -> None:
40
+ self._data_dir = Path(data_dir).expanduser()
41
+ self._timeout_s = timeout_s
42
+
43
+ if binary_path is not None:
44
+ self._binary = Path(binary_path).expanduser()
45
+ else:
46
+ self._binary = self._data_dir / "bin" / "blockfill"
47
+
48
+ self._config_path = self._data_dir / "config.toml"
49
+
50
+ # ------------------------------------------------------------------
51
+ # Internal helpers
52
+ # ------------------------------------------------------------------
53
+
54
+ def _sock(self) -> Path:
55
+ return _daemon.sock_path(self._data_dir)
56
+
57
+ def _require_binary(self) -> None:
58
+ if not self._binary.exists():
59
+ raise BinaryNotFound(
60
+ f"blockfill binary not found at {self._binary}. Run install() first."
61
+ )
62
+
63
+ def _require_daemon(self) -> None:
64
+ if not self._sock().exists():
65
+ raise DaemonNotRunning(f"daemon socket not found at {self._sock()}")
66
+
67
+ # ------------------------------------------------------------------
68
+ # Install & version
69
+ # ------------------------------------------------------------------
70
+
71
+ def install(self, version: str = "latest", force: bool = False) -> str:
72
+ return _installer.install(self._binary, version=version, force=force)
73
+
74
+ def version(self) -> str:
75
+ self._require_binary()
76
+ return _installer.version_from_binary(self._binary)
77
+
78
+ # ------------------------------------------------------------------
79
+ # Credentials & config
80
+ # ------------------------------------------------------------------
81
+
82
+ def set_credentials(
83
+ self,
84
+ exchange: str,
85
+ api_key: str,
86
+ api_secret: str,
87
+ api_passphrase: str | None = None,
88
+ ) -> None:
89
+ _config.set_credentials(
90
+ self._config_path,
91
+ exchange=exchange,
92
+ api_key=api_key,
93
+ api_secret=api_secret,
94
+ api_passphrase=api_passphrase,
95
+ )
96
+
97
+ def set_qtex_endpoint(self, endpoint: str) -> None:
98
+ _config.set_qtex_endpoint(self._config_path, endpoint)
99
+
100
+ # ------------------------------------------------------------------
101
+ # Daemon management
102
+ # ------------------------------------------------------------------
103
+
104
+ def is_running(self) -> bool:
105
+ return _daemon.is_running(self._data_dir, timeout_s=2.0)
106
+
107
+ def start(
108
+ self,
109
+ wait_timeout_s: float = 10.0,
110
+ env: dict | None = None,
111
+ ) -> None:
112
+ self._require_binary()
113
+ _daemon.start(self._binary, self._data_dir, wait_timeout_s=wait_timeout_s, env=env)
114
+
115
+ def stop(self, wait_timeout_s: float = 5.0) -> None:
116
+ _daemon.stop(self._data_dir, wait_timeout_s=wait_timeout_s)
117
+
118
+ def restart(self) -> None:
119
+ self.stop()
120
+ self.start()
121
+
122
+ def health(self) -> DaemonStatus:
123
+ return _daemon.health(self._data_dir, timeout_s=self._timeout_s)
124
+
125
+ def run_foreground(self, env: dict | None = None) -> None:
126
+ self._require_binary()
127
+ import os
128
+ merged_env = {**os.environ, **(env or {})}
129
+ subprocess.run(
130
+ [str(self._binary), "--data-dir", str(self._data_dir), "run"],
131
+ env=merged_env,
132
+ )
133
+
134
+ # ------------------------------------------------------------------
135
+ # Ticket operations (UDS RPC)
136
+ # ------------------------------------------------------------------
137
+
138
+ def place(
139
+ self,
140
+ exchange: str,
141
+ symbol: str,
142
+ strategy: str,
143
+ target_position: float,
144
+ time_constraint_ms: int,
145
+ client_ticket_id: str | None = None,
146
+ ) -> Ticket:
147
+ self._require_daemon()
148
+ params: dict = {
149
+ "exchange": exchange,
150
+ "symbol": symbol,
151
+ "strategy": strategy,
152
+ "target_position": target_position,
153
+ "time_constraint_ms": time_constraint_ms,
154
+ }
155
+ if client_ticket_id is not None:
156
+ params["client_ticket_id"] = client_ticket_id
157
+
158
+ result = rpc.call(self._sock(), "ticket.place", params, timeout_s=self._timeout_s)
159
+ return _parse_ticket(result, defaults=params)
160
+
161
+ def query(
162
+ self,
163
+ status: str | None = None,
164
+ symbol: str | None = None,
165
+ ticket_id: str | None = None,
166
+ from_ms: int | None = None,
167
+ to_ms: int | None = None,
168
+ limit: int = 100,
169
+ ) -> list[Ticket]:
170
+ self._require_daemon()
171
+ params: dict = {"limit": limit}
172
+ if status is not None:
173
+ params["status"] = status
174
+ if symbol is not None:
175
+ params["symbol"] = symbol
176
+ if ticket_id is not None:
177
+ params["ticket_id"] = ticket_id
178
+ if from_ms is not None:
179
+ params["from_ms"] = from_ms
180
+ if to_ms is not None:
181
+ params["to_ms"] = to_ms
182
+
183
+ result = rpc.call(self._sock(), "ticket.query", params, timeout_s=self._timeout_s)
184
+ tickets = result if isinstance(result, list) else result.get("tickets", [])
185
+ return [_parse_ticket(t) for t in tickets]
186
+
187
+ def cancel(self, ticket_id: str) -> None:
188
+ self._require_daemon()
189
+ try:
190
+ rpc.call(
191
+ self._sock(),
192
+ "ticket.cancel",
193
+ {"ticket_id": ticket_id},
194
+ timeout_s=self._timeout_s,
195
+ )
196
+ except Exception as e:
197
+ # Surface TicketNotFound if the daemon signals it
198
+ msg = str(e).lower()
199
+ if "not found" in msg or "404" in msg:
200
+ raise TicketNotFound(ticket_id) from e
201
+ raise
202
+
203
+ def cancel_all(self) -> int:
204
+ self._require_daemon()
205
+ result = rpc.call(self._sock(), "ticket.cancel_all", timeout_s=self._timeout_s)
206
+ if isinstance(result, int):
207
+ return result
208
+ return result.get("canceled", 0)
209
+
210
+ # ------------------------------------------------------------------
211
+ # Context manager
212
+ # ------------------------------------------------------------------
213
+
214
+ def __enter__(self) -> Self:
215
+ return self
216
+
217
+ def __exit__(self, *_) -> None:
218
+ self.stop()
blockfill/config.py ADDED
@@ -0,0 +1,48 @@
1
+ import os
2
+ import stat
3
+ from pathlib import Path
4
+
5
+ import toml
6
+
7
+ from .exceptions import CredentialsError
8
+
9
+
10
+ def _load_toml(config_path: Path) -> dict:
11
+ if config_path.exists():
12
+ return toml.loads(config_path.read_text())
13
+ return {}
14
+
15
+
16
+ def _save_toml(config_path: Path, data: dict) -> None:
17
+ config_path.parent.mkdir(parents=True, exist_ok=True)
18
+ config_path.write_text(toml.dumps(data))
19
+ os.chmod(config_path, stat.S_IRUSR | stat.S_IWUSR)
20
+
21
+
22
+ def set_credentials(
23
+ config_path: Path,
24
+ exchange: str,
25
+ api_key: str,
26
+ api_secret: str,
27
+ api_passphrase: str | None = None,
28
+ ) -> None:
29
+ valid_exchanges = {"binance-futures", "okx-swap"}
30
+ if exchange not in valid_exchanges:
31
+ raise CredentialsError(f"Unknown exchange: {exchange!r}. Valid: {valid_exchanges}")
32
+
33
+ data = _load_toml(config_path)
34
+ if "exchanges" not in data:
35
+ data["exchanges"] = {}
36
+ entry: dict = {"api_key": api_key, "api_secret": api_secret}
37
+ if api_passphrase is not None:
38
+ entry["api_passphrase"] = api_passphrase
39
+ data["exchanges"][exchange] = entry
40
+ _save_toml(config_path, data)
41
+
42
+
43
+ def set_qtex_endpoint(config_path: Path, endpoint: str) -> None:
44
+ data = _load_toml(config_path)
45
+ if "qtex" not in data:
46
+ data["qtex"] = {}
47
+ data["qtex"]["endpoint"] = endpoint
48
+ _save_toml(config_path, data)
blockfill/daemon.py ADDED
@@ -0,0 +1,90 @@
1
+ import os
2
+ import subprocess
3
+ import time
4
+ from pathlib import Path
5
+
6
+ from . import rpc
7
+ from .exceptions import DaemonNotRunning, DaemonStartTimeout
8
+ from .models import DaemonStatus
9
+
10
+
11
+ def sock_path(data_dir: Path) -> Path:
12
+ return data_dir / "runtime" / "daemon.sock"
13
+
14
+
15
+ def is_running(data_dir: Path, timeout_s: float = 2.0) -> bool:
16
+ sp = sock_path(data_dir)
17
+ if not sp.exists():
18
+ return False
19
+ try:
20
+ rpc.call(sp, "system.status", timeout_s=timeout_s)
21
+ return True
22
+ except Exception:
23
+ return False
24
+
25
+
26
+ def start(
27
+ binary_path: Path,
28
+ data_dir: Path,
29
+ wait_timeout_s: float = 10.0,
30
+ env: dict | None = None,
31
+ ) -> None:
32
+ if is_running(data_dir):
33
+ return
34
+
35
+ merged_env = {**os.environ, **(env or {})}
36
+ subprocess.Popen(
37
+ [str(binary_path), "--data-dir", str(data_dir), "run"],
38
+ env=merged_env,
39
+ stdout=subprocess.DEVNULL,
40
+ stderr=subprocess.DEVNULL,
41
+ )
42
+
43
+ deadline = time.monotonic() + wait_timeout_s
44
+ while time.monotonic() < deadline:
45
+ if is_running(data_dir, timeout_s=1.0):
46
+ return
47
+ time.sleep(0.25)
48
+
49
+ raise DaemonStartTimeout(
50
+ f"daemon did not become ready within {wait_timeout_s}s "
51
+ f"(sock: {sock_path(data_dir)})"
52
+ )
53
+
54
+
55
+ def stop(data_dir: Path, wait_timeout_s: float = 5.0) -> None:
56
+ sp = sock_path(data_dir)
57
+ if not sp.exists():
58
+ return
59
+
60
+ try:
61
+ rpc.call(sp, "system.shutdown", timeout_s=3.0)
62
+ except Exception:
63
+ # If daemon is already gone or RPC fails, proceed to wait for socket removal
64
+ pass
65
+
66
+ deadline = time.monotonic() + wait_timeout_s
67
+ while time.monotonic() < deadline:
68
+ if not sp.exists():
69
+ return
70
+ time.sleep(0.25)
71
+
72
+
73
+ def health(data_dir: Path, timeout_s: float = 10.0) -> DaemonStatus:
74
+ sp = sock_path(data_dir)
75
+ if not sp.exists():
76
+ raise DaemonNotRunning(f"daemon socket not found at {sp}")
77
+
78
+ try:
79
+ result = rpc.call(sp, "system.status", timeout_s=timeout_s)
80
+ except Exception as e:
81
+ raise DaemonNotRunning(f"cannot connect to daemon: {e}") from e
82
+
83
+ return DaemonStatus(
84
+ running=result.get("running", True),
85
+ pid=result.get("pid", 0),
86
+ exchanges=result.get("exchanges", []),
87
+ active_tickets=result.get("active_tickets", 0),
88
+ uptime_s=result.get("uptime_s", 0),
89
+ version=result.get("version", ""),
90
+ )
@@ -0,0 +1,29 @@
1
+ class BinaryNotFound(Exception):
2
+ pass
3
+
4
+
5
+ class DaemonNotRunning(Exception):
6
+ pass
7
+
8
+
9
+ class DaemonStartTimeout(Exception):
10
+ pass
11
+
12
+
13
+ class RpcError(Exception):
14
+ def __init__(self, code: int, message: str) -> None:
15
+ super().__init__(f"RPC error {code}: {message}")
16
+ self.code = code
17
+ self.message = message
18
+
19
+
20
+ class TicketNotFound(Exception):
21
+ pass
22
+
23
+
24
+ class CredentialsError(Exception):
25
+ pass
26
+
27
+
28
+ class InstallError(Exception):
29
+ pass
blockfill/installer.py ADDED
@@ -0,0 +1,42 @@
1
+ import platform
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+ from .exceptions import BinaryNotFound, InstallError
6
+
7
+
8
+ def _detect_platform() -> str:
9
+ system = platform.system().lower()
10
+ machine = platform.machine().lower()
11
+
12
+ if system == "linux":
13
+ if machine in ("x86_64", "amd64"):
14
+ return "linux-amd64"
15
+ elif machine in ("aarch64", "arm64"):
16
+ return "linux-arm64"
17
+ elif system == "darwin":
18
+ return "darwin-arm64"
19
+
20
+ raise InstallError(f"Unsupported platform: {system}/{machine}")
21
+
22
+
23
+ def install(binary_path: Path, version: str = "latest", force: bool = False) -> str:
24
+ if binary_path.exists() and not force:
25
+ # Already installed — return current version without downloading
26
+ return version_from_binary(binary_path)
27
+
28
+ _detect_platform() # validate platform before attempting download
29
+ raise NotImplementedError("download not yet implemented")
30
+
31
+
32
+ def version_from_binary(binary_path: Path) -> str:
33
+ if not binary_path.exists():
34
+ raise BinaryNotFound(f"blockfill binary not found at {binary_path}. Run install() first.")
35
+ result = subprocess.run(
36
+ [str(binary_path), "version"],
37
+ capture_output=True,
38
+ text=True,
39
+ )
40
+ if result.returncode != 0:
41
+ raise BinaryNotFound(f"blockfill version failed: {result.stderr.strip()}")
42
+ return result.stdout.strip()
blockfill/models.py ADDED
@@ -0,0 +1,38 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class DaemonStatus:
6
+ running: bool
7
+ pid: int
8
+ exchanges: list[str]
9
+ active_tickets: int
10
+ uptime_s: int
11
+ version: str
12
+
13
+
14
+ @dataclass
15
+ class Ticket:
16
+ ticket_id: str
17
+ status: str
18
+ exchange: str
19
+ symbol: str
20
+ strategy: str
21
+ target_position: float
22
+ executed_position: float | None
23
+ time_constraint_ms: int
24
+ start_time_ms: int | None
25
+ last_update_time_ms: int | None
26
+ is_expired: bool
27
+ cancel_reason: str | None
28
+
29
+ def __str__(self) -> str:
30
+ parts = [f"{self.ticket_id} [{self.status}] {self.exchange}/{self.symbol}",
31
+ f"target={self.target_position}"]
32
+ if self.executed_position is not None:
33
+ parts.append(f"executed={self.executed_position}")
34
+ if self.cancel_reason:
35
+ parts.append(f"cancel_reason={self.cancel_reason}")
36
+ if self.is_expired:
37
+ parts.append("EXPIRED")
38
+ return " ".join(parts)
blockfill/rpc.py ADDED
@@ -0,0 +1,23 @@
1
+ import json
2
+ import socket
3
+ from pathlib import Path
4
+
5
+ from .exceptions import RpcError
6
+
7
+
8
+ def call(sock_path: Path | str, method: str, params: dict | None = None, timeout_s: float = 10.0) -> dict:
9
+ req = json.dumps({"jsonrpc": "2.0", "method": method, "params": params or {}, "id": 1}) + "\n"
10
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
11
+ s.settimeout(timeout_s)
12
+ s.connect(str(sock_path))
13
+ s.sendall(req.encode())
14
+ data = b""
15
+ while not data.endswith(b"\n"):
16
+ chunk = s.recv(4096)
17
+ if not chunk:
18
+ break
19
+ data += chunk
20
+ resp = json.loads(data.decode())
21
+ if "error" in resp:
22
+ raise RpcError(resp["error"]["code"], resp["error"]["message"])
23
+ return resp["result"]
@@ -0,0 +1,299 @@
1
+ Metadata-Version: 2.4
2
+ Name: blockfill
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the blockfill execution daemon
5
+ Author-email: Mavri-X <robot@mavri-x.ai>
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://github.com/mavri-x/blockfill
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: toml
11
+
12
+ # blockfill Python SDK
13
+
14
+ Python wrapper for the [blockfill](../executor/) execution daemon. Covers the full lifecycle: install binary, configure credentials, manage daemon, place/query/cancel tickets.
15
+
16
+ ## Requirements
17
+
18
+ - Python 3.10+
19
+ - blockfill binary (see [Install](#install))
20
+ - blockfill daemon running (see [Daemon](#daemon))
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install blockfill
26
+ # or from source
27
+ pip install -e /path/to/python-sdk
28
+ ```
29
+
30
+ ## Quickstart
31
+
32
+ ```python
33
+ from blockfill import Blockfill
34
+
35
+ bf = Blockfill(
36
+ binary_path="/path/to/blockfill", # or ~/.blockfill/bin/blockfill
37
+ data_dir="~/.blockfill",
38
+ )
39
+
40
+ # First-time setup
41
+ bf.set_credentials("binance-futures", api_key="...", api_secret="...")
42
+ bf.set_qtex_endpoint("https://qtex.example.com")
43
+
44
+ # Start daemon
45
+ bf.start(env={"BLOCKFILL_API_KEY": "..."})
46
+ bf.health()
47
+
48
+ # Place a ticket
49
+ ticket = bf.place(
50
+ exchange="binance-futures",
51
+ symbol="btcusdt",
52
+ strategy="maker_l2",
53
+ target_position=0.1,
54
+ time_constraint_ms=300_000,
55
+ )
56
+ print(ticket.ticket_id, ticket.status) # tkt_xxx NEW
57
+
58
+ # Query
59
+ tickets = bf.query(status="NEW")
60
+
61
+ # Cancel
62
+ bf.cancel(ticket.ticket_id)
63
+
64
+ # Stop daemon
65
+ bf.stop()
66
+ ```
67
+
68
+ ---
69
+
70
+ ## API Reference
71
+
72
+ ### `Blockfill(binary_path, data_dir, timeout_s)`
73
+
74
+ | Parameter | Default | Description |
75
+ | ------------- | ---------------------------- | ------------------------------------- |
76
+ | `binary_path` | `~/.blockfill/bin/blockfill` | Path to blockfill binary |
77
+ | `data_dir` | `~/.blockfill` | Data directory (config, socket, logs) |
78
+ | `timeout_s` | `10.0` | RPC call timeout (seconds) |
79
+
80
+ ---
81
+
82
+ ### Install & Version
83
+
84
+ ```python
85
+ bf.install(version="latest", force=False) -> str
86
+ # Returns installed version string.
87
+ # If binary already exists and force=False, skips download and returns current version.
88
+
89
+ bf.version() -> str
90
+ # Returns "blockfill 0.1.0"
91
+ ```
92
+
93
+ ---
94
+
95
+ ### Credentials
96
+
97
+ ```python
98
+ bf.set_credentials(
99
+ exchange: str, # "binance-futures" | "okx-swap"
100
+ api_key: str,
101
+ api_secret: str,
102
+ api_passphrase: str | None = None, # OKX only
103
+ ) -> None
104
+ # Writes to {data_dir}/config.toml (chmod 0600)
105
+
106
+ bf.set_qtex_endpoint(endpoint: str) -> None
107
+ # e.g. bf.set_qtex_endpoint("https://qtex.example.com")
108
+ ```
109
+
110
+ ---
111
+
112
+ ### Daemon
113
+
114
+ ```python
115
+ bf.start(wait_timeout_s=10.0, env=None) -> None
116
+ # Starts daemon in background. No-op if already running.
117
+ # env: extra environment variables (e.g. {"BLOCKFILL_API_KEY": "..."})
118
+
119
+ bf.stop(wait_timeout_s=5.0) -> None
120
+ # Graceful shutdown. No-op if not running.
121
+
122
+ bf.restart() -> None
123
+
124
+ bf.is_running() -> bool
125
+
126
+ bf.health() -> DaemonStatus
127
+ # Raises DaemonNotRunning if daemon is not up.
128
+
129
+ bf.run_foreground(env=None) -> None
130
+ # Blocking. For dev/debug.
131
+ ```
132
+
133
+ `DaemonStatus` fields: `running`, `pid`, `exchanges`, `active_tickets`, `uptime_s`, `version`
134
+
135
+ ---
136
+
137
+ ### Tickets
138
+
139
+ ```python
140
+ bf.place(
141
+ exchange: str,
142
+ symbol: str,
143
+ strategy: str,
144
+ target_position: float, # positive = buy, negative = sell
145
+ time_constraint_ms: int,
146
+ client_ticket_id: str | None = None, # idempotency key
147
+ ) -> Ticket
148
+
149
+ bf.query(
150
+ status: str | None = None, # "NEW" | "OPEN" | "COMPLETE" | "CANCEL"
151
+ symbol: str | None = None,
152
+ ticket_id: str | None = None,
153
+ from_ms: int | None = None,
154
+ to_ms: int | None = None,
155
+ limit: int = 100,
156
+ ) -> list[Ticket]
157
+
158
+ bf.cancel(ticket_id: str) -> None
159
+ # Raises TicketNotFound if ticket doesn't exist.
160
+
161
+ bf.cancel_all() -> int
162
+ # Returns number of cancelled tickets.
163
+ ```
164
+
165
+ `Ticket` fields: `ticket_id`, `status`, `exchange`, `symbol`, `strategy`, `target_position`, `executed_position`, `time_constraint_ms`, `start_time_ms`, `last_update_time_ms`, `is_expired`, `cancel_reason`
166
+
167
+ **Auto-cancel**: placing a new ticket for the same `exchange+symbol` automatically cancels existing `NEW` tickets for that pair.
168
+
169
+ ---
170
+
171
+ ### Context Manager
172
+
173
+ ```python
174
+ with Blockfill(binary_path=..., data_dir=...) as bf:
175
+ bf.start(env={"BLOCKFILL_API_KEY": "..."})
176
+ ticket = bf.place(...)
177
+ # daemon is stopped on exit
178
+ ```
179
+
180
+ ---
181
+
182
+ ### Exceptions
183
+
184
+ | Exception | When |
185
+ | ------------------------- | --------------------------------------------- |
186
+ | `BinaryNotFound` | binary not found, call `install()` first |
187
+ | `DaemonNotRunning` | daemon socket not found or unreachable |
188
+ | `DaemonStartTimeout` | `start()` timed out waiting for daemon |
189
+ | `RpcError(code, message)` | daemon returned a JSON-RPC error |
190
+ | `TicketNotFound` | `cancel()` called with non-existent ticket_id |
191
+ | `CredentialsError` | invalid exchange name in `set_credentials()` |
192
+ | `InstallError` | binary download failed |
193
+
194
+ ---
195
+
196
+ ## Patterns
197
+
198
+ ### Strategy system integration
199
+
200
+ ```python
201
+ from blockfill import Blockfill, DaemonNotRunning
202
+
203
+ bf = Blockfill(binary_path="/path/to/blockfill")
204
+
205
+ # On startup
206
+ if not bf.is_running():
207
+ bf.start(env={"BLOCKFILL_API_KEY": "..."})
208
+ bf.health()
209
+
210
+ # On each signal
211
+ ticket = bf.place(
212
+ exchange="binance-futures",
213
+ symbol=symbol,
214
+ strategy="maker_l2",
215
+ target_position=position,
216
+ time_constraint_ms=300_000,
217
+ )
218
+ ```
219
+
220
+ ### Idempotent placement (safe to retry)
221
+
222
+ ```python
223
+ import uuid
224
+
225
+ key = str(uuid.uuid4()) # generate once, reuse on retry
226
+
227
+ ticket = bf.place(
228
+ exchange="binance-futures",
229
+ symbol="btcusdt",
230
+ strategy="maker_l2",
231
+ target_position=0.1,
232
+ time_constraint_ms=300_000,
233
+ client_ticket_id=key,
234
+ )
235
+ ```
236
+
237
+ ### Check open positions
238
+
239
+ ```python
240
+ open_tickets = bf.query(status="NEW") + bf.query(status="OPEN")
241
+ for t in open_tickets:
242
+ print(t.symbol, t.target_position, t.executed_position)
243
+ ```
244
+
245
+ ---
246
+
247
+ ## AI Agent Usage
248
+
249
+ The SDK is designed for programmatic use by AI agents (Claude, GPT, etc.).
250
+
251
+ ```python
252
+ import sys
253
+ sys.path.insert(0, "/path/to/python-sdk")
254
+
255
+ import os
256
+ os.environ["BLOCKFILL_API_KEY"] = "your_api_key"
257
+
258
+ from blockfill import Blockfill, DaemonNotRunning, TicketNotFound
259
+
260
+ bf = Blockfill(
261
+ binary_path="/path/to/blockfill",
262
+ data_dir="/path/to/data_dir",
263
+ )
264
+
265
+ if not bf.is_running():
266
+ bf.start(env={"BLOCKFILL_API_KEY": os.environ["BLOCKFILL_API_KEY"]})
267
+
268
+ bf.health() # raises if daemon not ready
269
+ ```
270
+
271
+ Key points for agents:
272
+
273
+ - Always call `bf.health()` before placing tickets
274
+ - Use `client_ticket_id=uuid.uuid4()` for idempotent retries
275
+ - `place()` auto-cancels existing NEW tickets for the same exchange+symbol
276
+ - `query()` returns data from qtex MongoDB (not exchange realtime)
277
+
278
+ ---
279
+
280
+ ## Directory Structure
281
+
282
+ ```
283
+ ~/.blockfill/
284
+ ├── config.toml # credentials + qtex endpoint (chmod 0600)
285
+ ├── bin/
286
+ │ └── blockfill # binary
287
+ ├── runtime/
288
+ │ ├── daemon.sock # UDS socket (CLI ↔ daemon IPC)
289
+ │ ├── daemon.pid # PID file
290
+ │ └── daemon.log # daemon logs
291
+ └── trading-log/ # Channel B upload retry buffer
292
+ ```
293
+
294
+ ## Supported Exchanges
295
+
296
+ | Exchange | Value |
297
+ | --------------- | ------------------- |
298
+ | Binance Futures | `"binance-futures"` |
299
+ | OKX Swap | `"okx-swap"` |
@@ -0,0 +1,12 @@
1
+ blockfill/__init__.py,sha256=SRnwufNFyG0daHJUsJB4iZ3JlUazNSKb3T36XWj0YrY,459
2
+ blockfill/client.py,sha256=-88AvxXfHAVIrn2xRqj3-swaNFypaajD2kCDzW4MPBA,7416
3
+ blockfill/config.py,sha256=dOvarHB3_hTh64wQ7c6U2TCmQkpZdSgZHSkzWITwW9U,1347
4
+ blockfill/daemon.py,sha256=fXMbfeJOI2K0g1--tzRaeYExWCez8nPnLUr6atlvBcY,2418
5
+ blockfill/exceptions.py,sha256=5s9-vQ2qh8-YFfIAuLuHyrh_UdxJSemJ2dNt7dVnTxQ,467
6
+ blockfill/installer.py,sha256=SkUgd_kNIRnlALUECJp_O2yGoad52eEMB_eO2pMmLtw,1377
7
+ blockfill/models.py,sha256=JMp0-8JZM5-06HViYX9MKpQhQqWaAySdz8P2Xl3Itvc,971
8
+ blockfill/rpc.py,sha256=9tAyg3F8fTas5DaM12ML0HEoIhP9UEmnYLrZiZv3Src,781
9
+ blockfill-0.1.0.dist-info/METADATA,sha256=nW7skiyIVa-cvb3Uvuzh4TGAmynP7ROKG8ANN_-BfBw,7503
10
+ blockfill-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ blockfill-0.1.0.dist-info/top_level.txt,sha256=vGuWqvvXlW6NdWV0A4YoA5pbLXbUdD-m1QX5hZxgk3M,10
12
+ blockfill-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ blockfill