tribulnation-engine 0.1.0__tar.gz
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.
- tribulnation_engine-0.1.0/PKG-INFO +255 -0
- tribulnation_engine-0.1.0/README.md +237 -0
- tribulnation_engine-0.1.0/pyproject.toml +39 -0
- tribulnation_engine-0.1.0/setup.cfg +4 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/__init__.py +3 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/__init__.pyi +51 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/cli/__init__.py +18 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/cli/gateway.py +61 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/cli/init.py +116 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/cli/manager.py +124 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/cli/register.py +29 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/cli/service.py +232 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/cli/start.py +165 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/cli/stop.py +25 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/cli/task.py +72 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/client/__init__.py +9 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/client/__init__.pyi +7 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/client/engine.py +46 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/client/tasks.py +46 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/config.py +91 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/diagnostics.py +95 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/gateway/__init__.py +3 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/gateway/__init__.pyi +8 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/gateway/codec.py +504 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/gateway/proxy.py +435 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/gateway/server.py +309 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/manager/__init__.py +3 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/manager/__init__.pyi +12 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/manager/api.py +366 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/manager/loader.py +65 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/manager/manager.py +139 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/manager/store.py +305 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/py.typed +0 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/runtime/__init__.py +3 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/runtime/__init__.pyi +14 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/runtime/context.py +77 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/runtime/local.py +366 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/runtime/logging.py +36 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/runtime/process.py +203 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/ui/_app/immutable/assets/0.BS0LOXZX.css +1 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/ui/_app/immutable/assets/2.Cg5XSmJo.css +1 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/ui/_app/immutable/chunks/1VhAfQzX.js +1 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/ui/_app/immutable/chunks/9qXYRaA8.js +1 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/ui/_app/immutable/chunks/Bswyvc5d.js +1 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/ui/_app/immutable/chunks/GNg5PO02.js +1 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/ui/_app/immutable/chunks/wbPk3Yxo.js +81 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/ui/_app/immutable/entry/app.D8Kbc3Vj.js +2 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/ui/_app/immutable/entry/start.BdONDuMw.js +1 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/ui/_app/immutable/nodes/0.D5ZShN99.js +1 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/ui/_app/immutable/nodes/1.DnfkyBS5.js +1 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/ui/_app/immutable/nodes/2.BpaXpkZF.js +90 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/ui/_app/version.json +1 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/ui/favicon.svg +6 -0
- tribulnation_engine-0.1.0/src/tribulnation/engine/ui/index.html +41 -0
- tribulnation_engine-0.1.0/src/tribulnation_engine.egg-info/PKG-INFO +255 -0
- tribulnation_engine-0.1.0/src/tribulnation_engine.egg-info/SOURCES.txt +58 -0
- tribulnation_engine-0.1.0/src/tribulnation_engine.egg-info/dependency_links.txt +1 -0
- tribulnation_engine-0.1.0/src/tribulnation_engine.egg-info/entry_points.txt +2 -0
- tribulnation_engine-0.1.0/src/tribulnation_engine.egg-info/requires.txt +9 -0
- tribulnation_engine-0.1.0/src/tribulnation_engine.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tribulnation-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Trading engine — gateway, task manager, and process SDK
|
|
5
|
+
Author-email: Marcel Claramunt <marcel@tribulnation.com>
|
|
6
|
+
Project-URL: repo, https://github.com/tribulnation/engine.git
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: typer
|
|
10
|
+
Requires-Dist: aiohttp
|
|
11
|
+
Requires-Dist: aiofiles
|
|
12
|
+
Requires-Dist: fastapi
|
|
13
|
+
Requires-Dist: aiosqlite
|
|
14
|
+
Requires-Dist: httpx
|
|
15
|
+
Requires-Dist: pydantic
|
|
16
|
+
Requires-Dist: python-dotenv
|
|
17
|
+
Requires-Dist: uvicorn
|
|
18
|
+
|
|
19
|
+
# Trading Engine
|
|
20
|
+
|
|
21
|
+
Run algorithmic trading strategies as managed tasks — with a live dashboard, per-task logs, restart policies, and a market SDK that abstracts across venues.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
### 1 — Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install tribulnation-engine
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2 — Initialize
|
|
34
|
+
|
|
35
|
+
Initialize the engine in a new directory:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
engine init
|
|
39
|
+
# creates engine.toml, .env, and strats/ directory
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Edit `engine.toml` and `.env` to configure accounts and strategies.
|
|
43
|
+
|
|
44
|
+
### 3 — Write a strategy
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# strats/my_strat.py
|
|
48
|
+
from tribulnation.engine import Process, RestartPolicy
|
|
49
|
+
|
|
50
|
+
class MyStrat(Process):
|
|
51
|
+
name = 'my-strat'
|
|
52
|
+
restart = RestartPolicy(on='error', delay=5.0)
|
|
53
|
+
|
|
54
|
+
async def run(self, market: str):
|
|
55
|
+
mkt = await self.sdk.market(market)
|
|
56
|
+
book = await mkt.depth()
|
|
57
|
+
self.log.info('mark price: %s', book.mark_price)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`run()` parameters become the task's argument schema — they appear as a form in
|
|
61
|
+
the dashboard and are coerced to the declared types when a task is started.
|
|
62
|
+
|
|
63
|
+
### 4 — Start the engine
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
engine start # foreground, logs to stdout
|
|
67
|
+
engine start -d # background — writes .engine.pid and engine.log
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 5 — Run a strategy
|
|
71
|
+
|
|
72
|
+
From the dashboard at `http://localhost:3121` or via the CLI:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
engine task start my-strat bitcoin -a market=mexc:spot:BTCUSDT
|
|
76
|
+
# engine task start <process_name> <run_id> -a <arg_name>=<arg_value> ...
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 6 — Stop a strategy
|
|
80
|
+
|
|
81
|
+
From the dashboard or:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
engine task stop my-strat bitcoin
|
|
85
|
+
# engine task stop <process_name> <run_id>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Strategies
|
|
91
|
+
|
|
92
|
+
### The `Process` base
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from tribulnation.engine import Process, RestartPolicy
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Implement `async def run(self, ...)`. Anything `run()` needs is injected via
|
|
99
|
+
`self`:
|
|
100
|
+
|
|
101
|
+
| Attribute | What it is |
|
|
102
|
+
|-----------|------------|
|
|
103
|
+
| `self.sdk` | Market SDK — call `await self.sdk.market('venue::symbol')` |
|
|
104
|
+
| `self.log` | Logger scoped to this task |
|
|
105
|
+
| `self.run_id` | The run ID this task was started with |
|
|
106
|
+
| `self.set_state(s)` | Publish a named state (`'connected'`, `'quoting'`, …) |
|
|
107
|
+
| `self.require(cls, id)` | Declare a dependency on another running task |
|
|
108
|
+
| `self.ipc` | Call methods on `Service` processes |
|
|
109
|
+
|
|
110
|
+
### Restart policy
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
class Hedger(Process):
|
|
114
|
+
restart = RestartPolicy(on='error', delay=5.0)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| `on=` | Behaviour |
|
|
118
|
+
|-------|-----------|
|
|
119
|
+
| `'never'` | Stop on exit or error |
|
|
120
|
+
| `'error'` | Restart after an unhandled exception, with `delay` seconds between attempts |
|
|
121
|
+
| `'always'` | Restart after any exit |
|
|
122
|
+
|
|
123
|
+
`max_attempts=N` caps the number of consecutive restarts.
|
|
124
|
+
|
|
125
|
+
### Dependencies
|
|
126
|
+
|
|
127
|
+
Use `require()` to gate a task on another reaching a named state:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
class DydxMaker(Process):
|
|
131
|
+
async def run(self, maker: str, hedge: str) -> None:
|
|
132
|
+
async with self.require(Hedger, self.run_id, state='connected')(
|
|
133
|
+
listen=maker, hedge=hedge
|
|
134
|
+
):
|
|
135
|
+
# runs only while Hedger/{self.run_id} is alive and in state 'connected'
|
|
136
|
+
...
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`require()` also auto-starts the dependency if it isn't running, and cancels
|
|
140
|
+
the dependent if the dependency stops or errors.
|
|
141
|
+
|
|
142
|
+
### Logging
|
|
143
|
+
|
|
144
|
+
Standard `logging` calls are automatically captured and streamed to the
|
|
145
|
+
dashboard. No setup needed:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
log = logging.getLogger(__name__)
|
|
149
|
+
|
|
150
|
+
class Hedger(Process):
|
|
151
|
+
async def run(self, ...):
|
|
152
|
+
log.info('listener live: %s → %s', listen, hedge)
|
|
153
|
+
# or use the process logger:
|
|
154
|
+
self.log.info('works the same way')
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`INFO` and above are always captured. `DEBUG` records are captured only from
|
|
158
|
+
loggers whose name starts with a prefix listed in `[daemon] debug_loggers`:
|
|
159
|
+
|
|
160
|
+
```toml
|
|
161
|
+
[daemon]
|
|
162
|
+
debug_loggers = ["strats"]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Config reference (`engine.toml`)
|
|
168
|
+
|
|
169
|
+
```toml
|
|
170
|
+
[engine]
|
|
171
|
+
processes = [
|
|
172
|
+
"strats.hedger:Hedger",
|
|
173
|
+
"strats.dydx_maker:DydxMaker",
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
[daemon]
|
|
177
|
+
host = "localhost"
|
|
178
|
+
port = 3121
|
|
179
|
+
db = "engine.db" # SQLite path; default "engine.db"
|
|
180
|
+
max_logs = 500 # in-memory log lines per task; default unlimited
|
|
181
|
+
|
|
182
|
+
[accounts.dydx-main]
|
|
183
|
+
venue = "dydx"
|
|
184
|
+
mnemonic = "$DYDX_MNEMONIC"
|
|
185
|
+
|
|
186
|
+
[accounts.hyperliquid-main]
|
|
187
|
+
venue = "hyperliquid"
|
|
188
|
+
address = "$HYPERLIQUID_ADDRESS"
|
|
189
|
+
private_key = "$HYPERLIQUID_PRIVATE_KEY"
|
|
190
|
+
|
|
191
|
+
[[tasks]]
|
|
192
|
+
process = "hedger"
|
|
193
|
+
id = "btc"
|
|
194
|
+
args = { listen = "dydx:perp:BTC-USD", hedge = "hl::BTC" }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Account credentials are resolved from environment variables when the value
|
|
198
|
+
starts with `$`. `engine start` automatically loads `.env` from the same
|
|
199
|
+
directory as `engine.toml`.
|
|
200
|
+
|
|
201
|
+
`[[tasks]]` entries are started automatically when the engine starts. Each
|
|
202
|
+
entry needs `process` (the registered process name), `id` (the run ID), and
|
|
203
|
+
optionally `args` (a table of keyword arguments matching the process
|
|
204
|
+
signature).
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## CLI
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
engine start # launch gateway + manager (foreground)
|
|
212
|
+
engine start -d # background — writes .engine.pid + engine.log
|
|
213
|
+
engine stop # stop a background engine
|
|
214
|
+
engine task start hedger btc -a listen=dydx:perp:BTC-USD -a hedge=hyperliquid::BTC
|
|
215
|
+
engine task stop hedger btc
|
|
216
|
+
engine register strats.new_strat:NewStrat # hot-load a process class
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## HTTP API
|
|
222
|
+
|
|
223
|
+
| Method | Path | Description |
|
|
224
|
+
|--------|------|-------------|
|
|
225
|
+
| `GET` | `/api/tasks` | Running tasks |
|
|
226
|
+
| `GET` | `/api/tasks/history` | Completed tasks (SQLite) |
|
|
227
|
+
| `GET` | `/api/task/{process}/{id}` | Single task |
|
|
228
|
+
| `GET` | `/api/task/{process}/{id}/logs` | Task logs |
|
|
229
|
+
| `GET` | `/api/task/{process}/{id}/attempts` | Restart attempts |
|
|
230
|
+
| `POST` | `/api/task/{process}/{id}` | Start a task — body: `{"args": {...}}` |
|
|
231
|
+
| `DELETE` | `/api/task/{process}/{id}` | Stop a task |
|
|
232
|
+
| `DELETE` | `/api/task/{process}/{id}?purge=true` | Stop and remove from history |
|
|
233
|
+
| `DELETE` | `/api/task/{process}/{id}/logs` | Clear logs and attempt history |
|
|
234
|
+
| `GET` | `/api/processes` | Registered process classes |
|
|
235
|
+
| `POST` | `/api/processes` | Register a process at runtime |
|
|
236
|
+
| `GET` | `/api/health` | Health check |
|
|
237
|
+
| `GET` | `/api/ws` | WebSocket event feed |
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Architecture
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
engine start
|
|
245
|
+
├── engine gateway owns real venue SDK connections, serves the market RPC protocol
|
|
246
|
+
└── engine manager task control plane
|
|
247
|
+
├── in-process runtime — strategies run as asyncio tasks
|
|
248
|
+
├── REST + WebSocket API (FastAPI / uvicorn)
|
|
249
|
+
└── SQLite store — task history, logs, restart attempts
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Strategies never hold venue connections directly. `self.sdk` inside a task is a
|
|
253
|
+
`ProxySDK` that routes market calls to the gateway over a local Unix socket. The
|
|
254
|
+
gateway owns the real credentials and connections; the manager owns task
|
|
255
|
+
lifecycle and the dashboard.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Trading Engine
|
|
2
|
+
|
|
3
|
+
Run algorithmic trading strategies as managed tasks — with a live dashboard, per-task logs, restart policies, and a market SDK that abstracts across venues.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Quickstart
|
|
8
|
+
|
|
9
|
+
### 1 — Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install tribulnation-engine
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### 2 — Initialize
|
|
16
|
+
|
|
17
|
+
Initialize the engine in a new directory:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
engine init
|
|
21
|
+
# creates engine.toml, .env, and strats/ directory
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Edit `engine.toml` and `.env` to configure accounts and strategies.
|
|
25
|
+
|
|
26
|
+
### 3 — Write a strategy
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
# strats/my_strat.py
|
|
30
|
+
from tribulnation.engine import Process, RestartPolicy
|
|
31
|
+
|
|
32
|
+
class MyStrat(Process):
|
|
33
|
+
name = 'my-strat'
|
|
34
|
+
restart = RestartPolicy(on='error', delay=5.0)
|
|
35
|
+
|
|
36
|
+
async def run(self, market: str):
|
|
37
|
+
mkt = await self.sdk.market(market)
|
|
38
|
+
book = await mkt.depth()
|
|
39
|
+
self.log.info('mark price: %s', book.mark_price)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`run()` parameters become the task's argument schema — they appear as a form in
|
|
43
|
+
the dashboard and are coerced to the declared types when a task is started.
|
|
44
|
+
|
|
45
|
+
### 4 — Start the engine
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
engine start # foreground, logs to stdout
|
|
49
|
+
engine start -d # background — writes .engine.pid and engine.log
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 5 — Run a strategy
|
|
53
|
+
|
|
54
|
+
From the dashboard at `http://localhost:3121` or via the CLI:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
engine task start my-strat bitcoin -a market=mexc:spot:BTCUSDT
|
|
58
|
+
# engine task start <process_name> <run_id> -a <arg_name>=<arg_value> ...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 6 — Stop a strategy
|
|
62
|
+
|
|
63
|
+
From the dashboard or:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
engine task stop my-strat bitcoin
|
|
67
|
+
# engine task stop <process_name> <run_id>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Strategies
|
|
73
|
+
|
|
74
|
+
### The `Process` base
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from tribulnation.engine import Process, RestartPolicy
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Implement `async def run(self, ...)`. Anything `run()` needs is injected via
|
|
81
|
+
`self`:
|
|
82
|
+
|
|
83
|
+
| Attribute | What it is |
|
|
84
|
+
|-----------|------------|
|
|
85
|
+
| `self.sdk` | Market SDK — call `await self.sdk.market('venue::symbol')` |
|
|
86
|
+
| `self.log` | Logger scoped to this task |
|
|
87
|
+
| `self.run_id` | The run ID this task was started with |
|
|
88
|
+
| `self.set_state(s)` | Publish a named state (`'connected'`, `'quoting'`, …) |
|
|
89
|
+
| `self.require(cls, id)` | Declare a dependency on another running task |
|
|
90
|
+
| `self.ipc` | Call methods on `Service` processes |
|
|
91
|
+
|
|
92
|
+
### Restart policy
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
class Hedger(Process):
|
|
96
|
+
restart = RestartPolicy(on='error', delay=5.0)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| `on=` | Behaviour |
|
|
100
|
+
|-------|-----------|
|
|
101
|
+
| `'never'` | Stop on exit or error |
|
|
102
|
+
| `'error'` | Restart after an unhandled exception, with `delay` seconds between attempts |
|
|
103
|
+
| `'always'` | Restart after any exit |
|
|
104
|
+
|
|
105
|
+
`max_attempts=N` caps the number of consecutive restarts.
|
|
106
|
+
|
|
107
|
+
### Dependencies
|
|
108
|
+
|
|
109
|
+
Use `require()` to gate a task on another reaching a named state:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
class DydxMaker(Process):
|
|
113
|
+
async def run(self, maker: str, hedge: str) -> None:
|
|
114
|
+
async with self.require(Hedger, self.run_id, state='connected')(
|
|
115
|
+
listen=maker, hedge=hedge
|
|
116
|
+
):
|
|
117
|
+
# runs only while Hedger/{self.run_id} is alive and in state 'connected'
|
|
118
|
+
...
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`require()` also auto-starts the dependency if it isn't running, and cancels
|
|
122
|
+
the dependent if the dependency stops or errors.
|
|
123
|
+
|
|
124
|
+
### Logging
|
|
125
|
+
|
|
126
|
+
Standard `logging` calls are automatically captured and streamed to the
|
|
127
|
+
dashboard. No setup needed:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
log = logging.getLogger(__name__)
|
|
131
|
+
|
|
132
|
+
class Hedger(Process):
|
|
133
|
+
async def run(self, ...):
|
|
134
|
+
log.info('listener live: %s → %s', listen, hedge)
|
|
135
|
+
# or use the process logger:
|
|
136
|
+
self.log.info('works the same way')
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`INFO` and above are always captured. `DEBUG` records are captured only from
|
|
140
|
+
loggers whose name starts with a prefix listed in `[daemon] debug_loggers`:
|
|
141
|
+
|
|
142
|
+
```toml
|
|
143
|
+
[daemon]
|
|
144
|
+
debug_loggers = ["strats"]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Config reference (`engine.toml`)
|
|
150
|
+
|
|
151
|
+
```toml
|
|
152
|
+
[engine]
|
|
153
|
+
processes = [
|
|
154
|
+
"strats.hedger:Hedger",
|
|
155
|
+
"strats.dydx_maker:DydxMaker",
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
[daemon]
|
|
159
|
+
host = "localhost"
|
|
160
|
+
port = 3121
|
|
161
|
+
db = "engine.db" # SQLite path; default "engine.db"
|
|
162
|
+
max_logs = 500 # in-memory log lines per task; default unlimited
|
|
163
|
+
|
|
164
|
+
[accounts.dydx-main]
|
|
165
|
+
venue = "dydx"
|
|
166
|
+
mnemonic = "$DYDX_MNEMONIC"
|
|
167
|
+
|
|
168
|
+
[accounts.hyperliquid-main]
|
|
169
|
+
venue = "hyperliquid"
|
|
170
|
+
address = "$HYPERLIQUID_ADDRESS"
|
|
171
|
+
private_key = "$HYPERLIQUID_PRIVATE_KEY"
|
|
172
|
+
|
|
173
|
+
[[tasks]]
|
|
174
|
+
process = "hedger"
|
|
175
|
+
id = "btc"
|
|
176
|
+
args = { listen = "dydx:perp:BTC-USD", hedge = "hl::BTC" }
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Account credentials are resolved from environment variables when the value
|
|
180
|
+
starts with `$`. `engine start` automatically loads `.env` from the same
|
|
181
|
+
directory as `engine.toml`.
|
|
182
|
+
|
|
183
|
+
`[[tasks]]` entries are started automatically when the engine starts. Each
|
|
184
|
+
entry needs `process` (the registered process name), `id` (the run ID), and
|
|
185
|
+
optionally `args` (a table of keyword arguments matching the process
|
|
186
|
+
signature).
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## CLI
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
engine start # launch gateway + manager (foreground)
|
|
194
|
+
engine start -d # background — writes .engine.pid + engine.log
|
|
195
|
+
engine stop # stop a background engine
|
|
196
|
+
engine task start hedger btc -a listen=dydx:perp:BTC-USD -a hedge=hyperliquid::BTC
|
|
197
|
+
engine task stop hedger btc
|
|
198
|
+
engine register strats.new_strat:NewStrat # hot-load a process class
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## HTTP API
|
|
204
|
+
|
|
205
|
+
| Method | Path | Description |
|
|
206
|
+
|--------|------|-------------|
|
|
207
|
+
| `GET` | `/api/tasks` | Running tasks |
|
|
208
|
+
| `GET` | `/api/tasks/history` | Completed tasks (SQLite) |
|
|
209
|
+
| `GET` | `/api/task/{process}/{id}` | Single task |
|
|
210
|
+
| `GET` | `/api/task/{process}/{id}/logs` | Task logs |
|
|
211
|
+
| `GET` | `/api/task/{process}/{id}/attempts` | Restart attempts |
|
|
212
|
+
| `POST` | `/api/task/{process}/{id}` | Start a task — body: `{"args": {...}}` |
|
|
213
|
+
| `DELETE` | `/api/task/{process}/{id}` | Stop a task |
|
|
214
|
+
| `DELETE` | `/api/task/{process}/{id}?purge=true` | Stop and remove from history |
|
|
215
|
+
| `DELETE` | `/api/task/{process}/{id}/logs` | Clear logs and attempt history |
|
|
216
|
+
| `GET` | `/api/processes` | Registered process classes |
|
|
217
|
+
| `POST` | `/api/processes` | Register a process at runtime |
|
|
218
|
+
| `GET` | `/api/health` | Health check |
|
|
219
|
+
| `GET` | `/api/ws` | WebSocket event feed |
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Architecture
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
engine start
|
|
227
|
+
├── engine gateway owns real venue SDK connections, serves the market RPC protocol
|
|
228
|
+
└── engine manager task control plane
|
|
229
|
+
├── in-process runtime — strategies run as asyncio tasks
|
|
230
|
+
├── REST + WebSocket API (FastAPI / uvicorn)
|
|
231
|
+
└── SQLite store — task history, logs, restart attempts
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Strategies never hold venue connections directly. `self.sdk` inside a task is a
|
|
235
|
+
`ProxySDK` that routes market calls to the gateway over a local Unix socket. The
|
|
236
|
+
gateway owns the real credentials and connections; the manager owns task
|
|
237
|
+
lifecycle and the dashboard.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tribulnation-engine"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{name="Marcel Claramunt", email="marcel@tribulnation.com"}
|
|
10
|
+
]
|
|
11
|
+
description = "Trading engine — gateway, task manager, and process SDK"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"typer",
|
|
14
|
+
"aiohttp",
|
|
15
|
+
"aiofiles",
|
|
16
|
+
"fastapi",
|
|
17
|
+
"aiosqlite",
|
|
18
|
+
"httpx",
|
|
19
|
+
"pydantic",
|
|
20
|
+
"python-dotenv",
|
|
21
|
+
"uvicorn",
|
|
22
|
+
]
|
|
23
|
+
requires-python = ">=3.11"
|
|
24
|
+
readme = {file="README.md", content-type="text/markdown"}
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.package-data]
|
|
27
|
+
"tribulnation.engine" = ["ui/**/*"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
repo = "https://github.com/tribulnation/engine.git"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
engine = "tribulnation.engine.cli:app"
|
|
34
|
+
|
|
35
|
+
# [project.optional-dependencies]
|
|
36
|
+
# test = [
|
|
37
|
+
# "pytest < 5.0.0",
|
|
38
|
+
# "pytest-cov[all]"
|
|
39
|
+
# ]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Tribulnation Engine — gateway, task manager, and strategy SDK.
|
|
2
|
+
|
|
3
|
+
Quick-start::
|
|
4
|
+
|
|
5
|
+
# 1. Define a worker:
|
|
6
|
+
from tribulnation.engine import Process
|
|
7
|
+
|
|
8
|
+
class Hedge(Process):
|
|
9
|
+
async def run(self, listen: str, market: str) -> None:
|
|
10
|
+
mkt = await self.sdk.perp_market(listen)
|
|
11
|
+
self.set_state('connected')
|
|
12
|
+
async for trade in await mkt.trades_stream():
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
# 2. Connect a client:
|
|
16
|
+
from tribulnation.engine import TradingEngine
|
|
17
|
+
|
|
18
|
+
async with TradingEngine.at('unix:///tmp/engine-gateway.sock', api_url='http://localhost:3121') as e:
|
|
19
|
+
book = await (await e.sdk.perp_market('dydx:perp:BTC-USD')).depth()
|
|
20
|
+
await e.task.start('hedge', 'btc', market='dydx:perp:BTC-USD', listen='hyper:perp:BTC')
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .client import TaskClient, TradingEngine
|
|
24
|
+
from .config import DaemonConfig, EngineConfig
|
|
25
|
+
from .gateway import EngineError, EngineGateway, ProxySDK
|
|
26
|
+
from .runtime import (
|
|
27
|
+
Process,
|
|
28
|
+
RPC,
|
|
29
|
+
Require,
|
|
30
|
+
RestartPolicy,
|
|
31
|
+
Runtime,
|
|
32
|
+
)
|
|
33
|
+
from .manager import Manager, Store, load_process
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
'Process',
|
|
37
|
+
'RPC',
|
|
38
|
+
'Require',
|
|
39
|
+
'RestartPolicy',
|
|
40
|
+
'Runtime',
|
|
41
|
+
'EngineGateway',
|
|
42
|
+
'ProxySDK',
|
|
43
|
+
'EngineError',
|
|
44
|
+
'TradingEngine',
|
|
45
|
+
'TaskClient',
|
|
46
|
+
'EngineConfig',
|
|
47
|
+
'DaemonConfig',
|
|
48
|
+
'Manager',
|
|
49
|
+
'Store',
|
|
50
|
+
'load_process',
|
|
51
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from .init import init
|
|
4
|
+
from .start import start
|
|
5
|
+
from .stop import stop
|
|
6
|
+
from .gateway import gateway
|
|
7
|
+
from .manager import manager
|
|
8
|
+
from .register import register
|
|
9
|
+
from .task import task_app
|
|
10
|
+
|
|
11
|
+
app = typer.Typer()
|
|
12
|
+
app.command()(init)
|
|
13
|
+
app.command()(start)
|
|
14
|
+
app.command()(stop)
|
|
15
|
+
app.command()(gateway)
|
|
16
|
+
app.command()(manager)
|
|
17
|
+
app.command()(register)
|
|
18
|
+
app.add_typer(task_app, name='task')
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""CLI command: ``engine gateway`` — start the standalone gateway process."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing_extensions import Annotated
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from dotenv import load_dotenv
|
|
12
|
+
|
|
13
|
+
_GATEWAY_LOGGERS = ('tribulnation.engine.gateway.server',)
|
|
14
|
+
|
|
15
|
+
_FMT = '%(asctime)s %(name)-40s %(message)s'
|
|
16
|
+
_DATEFMT = '%H:%M:%S'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def gateway(
|
|
20
|
+
socket: Annotated[
|
|
21
|
+
str | None,
|
|
22
|
+
typer.Option('--socket', '-s', help='Unix socket path (overrides engine.toml).'),
|
|
23
|
+
] = None,
|
|
24
|
+
config: Annotated[
|
|
25
|
+
Path, typer.Option('--config', '-c', help='Path to TOML config file.')
|
|
26
|
+
] = Path('engine.toml'),
|
|
27
|
+
verbose: Annotated[
|
|
28
|
+
int,
|
|
29
|
+
typer.Option(
|
|
30
|
+
'--verbose',
|
|
31
|
+
'-v',
|
|
32
|
+
count=True,
|
|
33
|
+
help='Increase log verbosity. Repeat for all debug output.',
|
|
34
|
+
),
|
|
35
|
+
] = 0,
|
|
36
|
+
):
|
|
37
|
+
"""Start the WebSocket gateway on a Unix socket."""
|
|
38
|
+
_configure_logging(verbose)
|
|
39
|
+
load_dotenv(config.parent / '.env')
|
|
40
|
+
|
|
41
|
+
from tribulnation.engine.config import EngineConfig
|
|
42
|
+
from tribulnation.engine.gateway.server import run_gateway
|
|
43
|
+
|
|
44
|
+
cfg = EngineConfig.load(config)
|
|
45
|
+
sock = socket or cfg.daemon.socket
|
|
46
|
+
asyncio.run(run_gateway(sock, cfg))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _configure_logging(verbose: int) -> None:
|
|
50
|
+
if verbose >= 2:
|
|
51
|
+
logging.basicConfig(level=logging.DEBUG, format=_FMT, datefmt=_DATEFMT)
|
|
52
|
+
return
|
|
53
|
+
if verbose:
|
|
54
|
+
handler = logging.StreamHandler()
|
|
55
|
+
handler.setLevel(logging.DEBUG)
|
|
56
|
+
handler.setFormatter(logging.Formatter(_FMT, datefmt=_DATEFMT))
|
|
57
|
+
root = logging.getLogger()
|
|
58
|
+
root.setLevel(logging.WARNING)
|
|
59
|
+
root.addHandler(handler)
|
|
60
|
+
for name in _GATEWAY_LOGGERS:
|
|
61
|
+
logging.getLogger(name).setLevel(logging.DEBUG)
|