Trading Engine
Run algorithmic trading strategies as managed tasks — with a live dashboard, per-task logs, restart policies, and a market SDK that abstracts across venues.
Quickstart
1 — Install
pip install tribulnation-engine
2 — Initialize
Initialize the engine in a new directory:
engine init
# creates engine.toml, .env, and strats/ directory
Edit engine.toml and .env to configure accounts and strategies.
3 — Write a strategy
# strats/my_strat.py
from tribulnation.engine import Process, RestartPolicy
class MyStrat(Process):
name = 'my-strat'
restart = RestartPolicy(on='error', delay=5.0)
async def run(self, market: str):
mkt = await self.sdk.market(market)
book = await mkt.depth()
self.log.info('mark price: %s', book.mark_price)
run() parameters become the task's argument schema — they appear as a form in
the dashboard and are coerced to the declared types when a task is started.
4 — Start the engine
engine start # foreground, logs to stdout
engine start -d # background — writes .engine.pid and engine.log
For longer-running deployments on a systemd host, you can also install it as a user service:
engine service install --cwd . --memory 2G -v
engine service status
engine service logs -f
The generated service runs engine start, restarts automatically, and uses
systemd MemoryMax when --memory is provided. It also uses a bounded stop
timeout so stuck shutdowns are force-killed by systemd:
engine service install --cwd . --memory 2G --timeout-stop-sec 20
Manage it with:
engine service restart
engine service stop
engine service uninstall
5 — Run a strategy
From the dashboard at http://localhost:3121 or via the CLI:
engine task start my-strat bitcoin -a market=mexc:spot:BTCUSDT
# engine task start <process_name> <run_id> -a <arg_name>=<arg_value> ...
6 — Stop a strategy
From the dashboard or:
engine task stop my-strat bitcoin
# engine task stop <process_name> <run_id>
Strategies
The Process base
from tribulnation.engine import Process, RestartPolicy
Implement async def run(self, ...). Anything run() needs is injected via
self:
| Attribute | What it is |
|---|---|
self.sdk |
Market SDK — call await self.sdk.market('venue::symbol') |
self.log |
Logger scoped to this task |
self.run_id |
The run ID this task was started with |
self.set_state(s) |
Publish a named state ('connected', 'quoting', …) |
self.require(cls, id) |
Declare a dependency on another running task |
self.ipc |
Call methods on Service processes |
Restart policy
class Hedger(Process):
restart = RestartPolicy(on='error', delay=5.0)
on= |
Behaviour |
|---|---|
'never' |
Stop on exit or error |
'error' |
Restart after an unhandled exception, with delay seconds between attempts |
'always' |
Restart after any exit |
max_attempts=N caps the number of consecutive restarts.
Dependencies
Use require() to gate a task on another reaching a named state:
class DydxMaker(Process):
async def run(self, maker: str, hedge: str) -> None:
async with self.require(Hedger, self.run_id, state='connected')(
listen=maker, hedge=hedge
):
# runs only while Hedger/{self.run_id} is alive and in state 'connected'
...
require() also auto-starts the dependency if it isn't running, and cancels
the dependent if the dependency stops or errors.
Logging
Standard logging calls are automatically captured and streamed to the
dashboard. No setup needed:
log = logging.getLogger(__name__)
class Hedger(Process):
async def run(self, ...):
log.info('listener live: %s → %s', listen, hedge)
# or use the process logger:
self.log.info('works the same way')
INFO and above are always captured. DEBUG records are captured only from
loggers whose name starts with a prefix listed in [daemon] debug_loggers:
[daemon]
debug_loggers = ["strats"]
Config reference (engine.toml)
[engine]
processes = [
"strats.hedger:Hedger",
"strats.dydx_maker:DydxMaker",
]
[daemon]
host = "localhost"
port = 3121
db = "engine.db" # SQLite path; default "engine.db"
max_logs = 500 # in-memory log lines per task; default unlimited
[accounts.dydx-main]
venue = "dydx"
mnemonic = "$DYDX_MNEMONIC"
[accounts.hyperliquid-main]
venue = "hyperliquid"
address = "$HYPERLIQUID_ADDRESS"
private_key = "$HYPERLIQUID_PRIVATE_KEY"
[[tasks]]
process = "hedger"
id = "btc"
args = { listen = "dydx:perp:BTC-USD", hedge = "hl::BTC" }
Account credentials are resolved from environment variables when the value
starts with $. engine start automatically loads .env from the same
directory as engine.toml.
[[tasks]] entries are started automatically when the engine starts. Each
entry needs process (the registered process name), id (the run ID), and
optionally args (a table of keyword arguments matching the process
signature).
CLI
engine start # launch gateway + manager (foreground)
engine start -d # background — writes .engine.pid + engine.log
engine service install --cwd . --memory 2G -v # install as a user systemd service
engine service logs -f # follow service logs
engine stop # stop a background engine
engine task start hedger btc -a listen=dydx:perp:BTC-USD -a hedge=hyperliquid::BTC
engine task stop hedger btc
engine register strats.new_strat:NewStrat # hot-load a process class
HTTP API
| Method | Path | Description |
|---|---|---|
GET |
/api/tasks |
Running tasks |
GET |
/api/tasks/history |
Completed tasks (SQLite) |
GET |
/api/task/{process}/{id} |
Single task |
GET |
/api/task/{process}/{id}/logs |
Task logs |
GET |
/api/task/{process}/{id}/attempts |
Restart attempts |
POST |
/api/task/{process}/{id} |
Start a task — body: {"args": {...}} |
DELETE |
/api/task/{process}/{id} |
Stop a task |
DELETE |
/api/task/{process}/{id}?purge=true |
Stop and remove from history |
DELETE |
/api/task/{process}/{id}/logs |
Clear logs and attempt history |
GET |
/api/processes |
Registered process classes |
POST |
/api/processes |
Register a process at runtime |
GET |
/api/health |
Health check |
GET |
/api/ws |
WebSocket event feed |
Architecture
engine start
├── engine gateway owns real venue SDK connections, serves the market RPC protocol
└── engine manager task control plane
├── in-process runtime — strategies run as asyncio tasks
├── REST + WebSocket API (FastAPI / uvicorn)
└── SQLite store — task history, logs, restart attempts
Strategies never hold venue connections directly. self.sdk inside a task is a
ProxySDK that routes market calls to the gateway over a local Unix socket. The
gateway owns the real credentials and connections; the manager owns task
lifecycle and the dashboard.
SDK 2 compatibility
Engine 0.1.19 requires SDK >=2.3.0,<3, dYdX >=0.8.0 and Hyperliquid >=0.8.1.
Upgrade the gateway and manager together: the RPC protocol now uses funding_rates
and preserves NextFunding.interval, account Fees, candle values and ticker volumes.
Spot and perpetual proxies expose the appropriate SDK product interfaces, including
market/exchange collateral. SDK account IDs and qualified exchange IDs are preserved.
The gateway enters one SDK root for its lifetime. Markets on the same account share clients, caches and subscriptions. Unsubscribing streams finish cleanup before the gateway closes its venue resources.
A local testnet smoke check is available without starting the manager or its tasks:
python dev/testnet_smoke.py --env-file ../strats/engine/.env
python dev/testnet_smoke.py --env-file ../strats/engine/.env --orders
It requires DYDX_TESTNET_MNEMONIC, DYDX_TESTNET_ADDRESS,
HYPERLIQUID_TESTNET_PRIVATE_KEY and HYPERLIQUID_TESTNET_ADDRESS in that file.
The optional --orders check places a small post-only buy on each testnet, observes
it, cancels that exact order, and verifies cancellation. Other open orders are untouched.
Releases
GitHub Actions builds and publishes releases. PRs and pushes to main type-check the package, run its Python tests,
build the dashboard using npm ci, and build/check the Python wheel and source
distribution. CI verifies that
the wheel contains the dashboard and uploads both distributions as workflow artifacts.
- Create a branch named
release/enginefrommain. - Bump
versioninlib/pyproject.tomland open a PR tomain. - Merge after checks pass. The release workflow checks the version bump and rebuilds
the exact merged commit, publishes its distributions to PyPI, then creates a
v<version>tag and GitHub release with the distributions attached.
Ordinary PR merges do not publish. Re-running the release workflow skips files already uploaded to PyPI and preserves an existing GitHub release.
Configure a PyPI trusted publisher
once for tribulnation-engine: owner tribulnation, repository engine, workflow
release.yml, with the environment field left empty (matching the SDK setup).
No PyPI API token or exchange credentials are required by CI.
Release files for tribulnation-engine 0.1.20
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tribulnation_engine-0.1.20.tar.gz | 117.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tribulnation_engine-0.1.20-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 248.5 kB
Release files / tribulnation_engine-0.1.20.tar.gz
| Download URL | tribulnation_engine-0.1.20.tar.gz |
|---|---|
| Size | 117.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e71ac665615785b52c487cc8a1862ddd662aefcd19562b401c9ad74fbfe67bb8
|
|
BLAKE2b-256 checksum How to use checksums |
8aca4c9563dccbee3d6df8fc521e1c888a24439098d4abc144a3696a616799d7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency logRelease files / tribulnation_engine-0.1.20-py3-none-any.whl
| Download URL | tribulnation_engine-0.1.20-py3-none-any.whl |
|---|---|
| Size | 131.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b82cfcf422c5f0abb1da309f68e56051d597deb2963264ac29a19443cbaa51d1
|
|
BLAKE2b-256 checksum How to use checksums |
cdd009bad1aecd7e37291b92c810b68718c2bfb32936e3cbb09c1369bc050463
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency log