Metadata-Version: 2.4
Name: dollarsmore-faro
Version: 0.8.4
Summary: tastytrade order-execution consumer / SDK: drains order intents off Redis Streams and places them
Project-URL: Homepage, https://github.com/dollarsmore/faro
Project-URL: Repository, https://github.com/dollarsmore/faro
Project-URL: Issues, https://github.com/dollarsmore/faro/issues
Author: Robert Krzysztoforski
License-Expression: MIT
License-File: LICENSE
Keywords: broker,orders,redis,tastytrade,trading
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.11
Requires-Dist: dollarsmore-wire>=0.3.0
Requires-Dist: redis<9,>=8.0.1
Requires-Dist: requests<3,>=2.34.2
Requires-Dist: sentry-sdk<3,>=2.64.0
Description-Content-Type: text/markdown

<p align="center">
  <img src="assets/faro-readme-header.png" alt="faro — a tastytrade order-execution consumer" width="100%">
</p>

# faro

A tastytrade order-execution consumer (and the seed of a small tastytrade SDK).
`faro` drains order intents off a Redis Stream and places them against
tastytrade, reporting results back on a results stream. It owns a minimal,
in-house tastytrade REST client (not a third-party broker library), so the
credential and money path is auditable end to end.

The wire schema it speaks is [`wire`](../wire) (`dollarsmore-wire`); a producer
emits `OrderIntent`s, `faro` consumes them and emits `ResultEvent`s.

## Install

```bash
pip install dollarsmore-faro
# or
uv add dollarsmore-faro
```

The distribution is `dollarsmore-faro`; the import package is `faro`.

## Run

```bash
python -m faro                 # consumes intents, places against the tastytrade production broker
python -m faro --account ... --redis-url redis://host:6379/0 --consumer faro-1
```

Credentials and wiring come from the environment (never baked into the image):

| Variable | Purpose |
|----------|---------|
| `TASTYTRADE_CLIENT_SECRET` | OAuth2 client secret |
| `TASTYTRADE_REFRESH_TOKEN` | OAuth2 refresh token |
| `TASTYTRADE_ACCOUNT` | account number (optional if exactly one account) |
| `REDIS_URL` | Redis connection (default `redis://localhost:6379/0`) |
| `FARO_CASEKEEPER_DIR` | audit-log directory (default `/app/casekeeper`) |
| `FARO_ARCHIVE_DIR` | stream-archive directory (default `/app/archive`) |

A second process, `python -m faro.archive`, tails every order/ops stream into a
durable per-day archive and trims the streams so Redis stays bounded.

## Runtime mode

`faro` reads the Redis key `faro:mode` each loop. Only the exact value `live` arms real
placement; an absent or unrecognised key reads as `dry` (fail-safe), so a fresh deploy never
trades until an operator deliberately sets `live`. In `dry` mode every placement is sent to the
broker's `dry-run` endpoint, which validates the order (buying power, fees, structure) without
executing it, while cancels and the corrective flatten are suppressed. The switch is hot (no
restart), and flipping `live`→`dry` while positions are open cancels any working orders and
flattens any held position at market first.

> **`faro` assumes it owns the trading account exclusively.** Reconciliation treats any
> position or order it did not place as a discrepancy, so in live mode it may auto-close
> such a position, and the `live`→`dry` flatten cancels and closes **everything** in the
> account — including orders or trades placed manually in the broker UI. Do not hand-trade
> the same account.

## Lifecycle

An order flows from a producer's intent on `orders:intents` to a terminal result on
`orders:results`. An `OpenBracketIntent` is placed as one OTO bracket at tastytrade (a limit
**entry** with a pre-staged **take-profit**); the entry then fills or expires, and a filled
position is closed by its take-profit or by a producer `CLOSE`. The whole map:

<p align="center">
  <img src="assets/lifecycle-overview.gif" alt="faro trade lifecycle, every path" width="100%">
</p>

Each path, watched live:

<table>
  <tr>
    <td width="50%"><img src="assets/lifecycle-take-profit.gif" alt="take-profit fills" width="100%"><br><b>Take-profit</b>: the entry fills, the pre-staged TP fills, the OTO self-closes.</td>
    <td width="50%"><img src="assets/lifecycle-not-filled.gif" alt="entry never fills" width="100%"><br><b>Not filled</b>: the entry limit never trades and expires at session end; nothing at risk.</td>
  </tr>
  <tr>
    <td width="50%"><img src="assets/lifecycle-stop-close.gif" alt="producer closes the position" width="100%"><br><b>Close</b>: a producer <code>CLOSE</code> cancels the resting TP and flattens the position.</td>
    <td width="50%"><img src="assets/lifecycle-rejected.gif" alt="broker rejects the order" width="100%"><br><b>Rejected</b>: the broker refuses the placement; no order works.</td>
  </tr>
</table>

A bracket can also be pulled before it fills (a producer `CANCEL`), and the `live`→`dry`
kill-switch cancels working orders and flattens any position at once. The animations are rendered
from [`tools/lifecycle.py`](tools/lifecycle.py) (`python -m tools.lifecycle`), not shipped in the
package.

## Layout

Two layers live side by side; the seam is where a future fuller SDK grows:

- **SDK core** — `session.py` (OAuth2 refresh-token session, never logs tokens,
  refuses non-tastyworks hosts), `client.py` (`TastytradeClient` REST),
  `symbols.py` (OCC option symbols), `executor.py` (`BrokerExecutor` port +
  `TastytradeExecutor` adapter — the swappable broker seam).
- **Consumer app** — `stream.py` (Redis Streams transport: intents / results /
  dead-letter), `service.py` (`FaroService` dispatch loop, crash-tolerant with
  PEL recovery), `fill_tracker.py` (polls the broker to confirm fills, records the
  actual price/time, emits `FILLED`), `book.py` (faro's believed state per trade),
  `reconciler.py` (diffs the book against the broker's positions, alerts on
  `ops:alerts`, mints a guarded corrective close), `casekeeper.py` (append-only
  per-day audit log of every intent received and broker result), `archive.py`
  (`python -m faro.archive`: tails the streams into a durable per-day archive and
  trims them), `heartbeat.py` (container healthcheck verdict), `__main__.py`
  (wires it together).

## Develop

```bash
uv sync                        # wire resolves from PyPI (dollarsmore-wire)
uv run pytest -q
uv run ruff check .
```

## Docker

```bash
docker build -t faro .         # installs dollarsmore-wire from PyPI
```

The image ships only `faro` + `wire` + `requests`/`redis` — no strategy code — so
it is safe to publish.
