Skip to main content

tastytrade order-execution consumer / SDK: drains order intents off Redis Streams and places them

Project description

faro — a tastytrade order-execution consumer

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 (dollarsmore-wire); a producer emits OrderIntents, faro consumes them and emits ResultEvents.

Install

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

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

Run

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 livedry while positions are open cancels any working orders and flattens any held position at market first.

Two exceptions to that suppression, both exposure-reducing only. A livedry flatten that cannot confirm the account flat records an owed flatten; while that marker stands, faro keeps cancelling the working bracket and market-closing the held position on a fixed retry, even though it is reporting dry mode, until the account is confirmed flat or a bounded number of attempts is exhausted (FLATTEN_OWED_* alerts track it). Opening is never forced: a forced close re-reads the live holding and places nothing when that read fails.

The retry is bounded in three independent ways, because a forced close is the one thing faro does against the operator's stated mode. It retires only on evidence that its own reason has passed, and the marker records which reason that is: a livedry disengage is moot the moment the operator asks for live again, so that aborts an unfinished disengage outright (FLATTEN_OWED_CANCELLED, and faro resumes managing the position, whose book entry was never reset), whereas a dead-man flatten retires only once the producer heart-beats again. Mode alone never drops a dead-man marker: the producer being silent is exactly when nobody else is managing the position. A marker written by an older faro, which carried no reason, reads as a disengage. It spends a fixed attempt budget that is returned only by reading the marker gone, so a clear that silently fails still ends at FLATTEN_OWED_ABANDONED rather than forcing a close every retry forever; a new incident carrying a different reason gets a fresh budget. Once the budget is spent faro stops forcing but keeps re-checking, so a position flattened by hand clears the marker (FLATTEN_OWED_RECOVERED) the moment the account reads flat, without a restart: from applied-dry no mode change would flatten it (arming is blocked by the marker, and a dry request early-returns), so that re-check is the only in-band way out. And an owed flatten blocks arming, so the two surfaces cannot disagree. A RECOVERED reports whether faro actually placed or cancelled anything (flatten) or merely found the account already flat (flatten:confirmed-flat), so the daily /compare gate does not grade a no-op settle as money moving.

Arming drylive is gated. faro refuses to arm while a broker holding or a resting order cannot be attributed to its own book, while intents are still unacked, while a reconciler discrepancy is unresolved, or while a flatten is still owed; the blocking field is named in the MODE_ENGAGE_BLOCKED alert, and any reading that faults counts as blocking. A holding its own book accounts for (the usual restart-mid-trade case) does not block, so faro resumes managing the position it already owns, and neither do faro's own corrective flatten orders. Disengaging is never gated.

The health record faro publishes gained unattributed_positions, unattributed_orders and flatten_owed in 0.10.0, and a consumer that derives its own arming view from that record (the operator dashboard does) will read a record without them as unreadable evidence and refuse to arm. Deploy faro before any such consumer, never after.

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 livedry 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:

faro trade lifecycle, every path

Each path, watched live:

take-profit fills
Take-profit: the entry fills, the pre-staged TP fills, the OTO self-closes.
entry never fills
Not filled: the entry limit never trades and expires at session end; nothing at risk.
producer closes the position
Close: a producer CLOSE cancels the resting TP and flattens the position.
broker rejects the order
Rejected: the broker refuses the placement; no order works.

A bracket can also be pulled before it fills (a producer CANCEL), and the livedry kill-switch cancels working orders and flattens any position at once. The animations are rendered from 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 coresession.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 appstream.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

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

Docker

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dollarsmore_faro-0.10.1.tar.gz (732.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dollarsmore_faro-0.10.1-py3-none-any.whl (57.1 kB view details)

Uploaded Python 3

File details

Details for the file dollarsmore_faro-0.10.1.tar.gz.

File metadata

  • Download URL: dollarsmore_faro-0.10.1.tar.gz
  • Upload date:
  • Size: 732.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for dollarsmore_faro-0.10.1.tar.gz
Algorithm Hash digest
SHA256 7e9f234c54b43aea45c084f072eac27df13c38a4b96972f77204424a0edab075
MD5 6ec145f1318bc3901c4444ee57fed05e
BLAKE2b-256 fdb22a5e971978538e98aa639799e49772f60dadc494e09da3f35bd1bcf1f4a1

See more details on using hashes here.

File details

Details for the file dollarsmore_faro-0.10.1-py3-none-any.whl.

File metadata

  • Download URL: dollarsmore_faro-0.10.1-py3-none-any.whl
  • Upload date:
  • Size: 57.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for dollarsmore_faro-0.10.1-py3-none-any.whl
Algorithm Hash digest
SHA256 58be4e28505977488c150ad0d135cd884b5d7f0f65fcfb3b5e395a6bd339d1e1
MD5 d2f53f119b0c1312200ddda7eb3293f0
BLAKE2b-256 2f7619c7da0e2220c461d3a5b6d55cf49214e06bea443d41e1d061ac60192b79

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page