Metadata-Version: 2.4
Name: tribulnation-engine
Version: 0.1.20
Summary: Trading engine — gateway, task manager, and process SDK
Author-email: Marcel Claramunt <marcel@tribulnation.com>
Project-URL: repo, https://github.com/tribulnation/engine.git
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: lazy-loader
Requires-Dist: tribulnation-sdk[dydx,hyperliquid]<3,>=2.3.0
Requires-Dist: tribulnation-dydx>=0.8.0
Requires-Dist: tribulnation-hyperliquid>=0.8.1
Requires-Dist: typer
Requires-Dist: aiohttp
Requires-Dist: aiofiles
Requires-Dist: fastapi
Requires-Dist: aiosqlite
Requires-Dist: httpx
Requires-Dist: pydantic
Requires-Dist: python-dotenv
Requires-Dist: uvicorn
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-asyncio; extra == "test"

# 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

```bash
pip install tribulnation-engine
```

### 2 — Initialize

Initialize the engine in a new directory:

```bash
engine init
# creates engine.toml, .env, and strats/ directory
```

Edit `engine.toml` and `.env` to configure accounts and strategies.

### 3 — Write a strategy

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

```bash
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:

```bash
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:

```bash
engine service install --cwd . --memory 2G --timeout-stop-sec 20
```

Manage it with:

```bash
engine service restart
engine service stop
engine service uninstall
```

### 5 — Run a strategy

From the dashboard at `http://localhost:3121` or via the CLI:

```bash
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:

```bash
engine task stop my-strat bitcoin
# engine task stop <process_name> <run_id>
```

---

## Strategies

### The `Process` base

```python
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

```python
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:

```python
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:

```python
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`:

```toml
[daemon]
debug_loggers = ["strats"]
```

---

## Config reference (`engine.toml`)

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

```bash
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:

```sh
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.

1. Create a branch named `release/engine` from `main`.
2. Bump `version` in `lib/pyproject.toml` and open a PR to `main`.
3. 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](https://docs.pypi.org/trusted-publishers/adding-a-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.
