Skip to main content

retrade: the Python SDK for reTrade

reTrade is a pre-trade check for AI trading agents. Before your agent's order goes to your broker, the SDK sends it to reTrade, which checks it against the rules your tenant's admins set, and answers at once:

  • ALLOW: the SDK places the order as sent.
  • CUT: the SDK places the smaller quantity reTrade approved.
  • BLOCK: nothing is placed. When the order was over an approval line or clashed with another agent's order, the answer includes fits, the largest quantity that would pass.

Every answer is signed by reTrade and chained into your tenant's log. The SDK checks each signature before it acts, places orders through your own broker connection (reTrade never connects to your broker), and keeps the signed records in a local file.

Status: in development. This is version 0.1.1 of the SDK, built to the wire spec retrade-arith/2.0-draft, which may still change: pin the version you test with. The SDK's default address is the hosted service, https://api.retrade.app; to use a reTrade server you run instead, pass base_url (how to run one).

Install

pip install retrade

Python 3.12 or later. The one dependency is cryptography. The package also installs the retrade command.

What you need from reTrade

What Looks like Who gives it to you
The server's address https://..., or http://127.0.0.1:8080 for a server on your machine the server's operator
The root public key 43 characters of base64url the server's operator
Your bot key id exk_enforce_01M... your tenant's admin, after registering your executor key
Your executor key a file such as executor.pem you make it, and it never leaves your machine

Make the executor key on the machine that will run your agent:

retrade keygen executor.pem

It writes the private key (file mode 0600) and prints its public half. Send only the public half to your tenant's admin. The admin registers it with retrade-admin keys add-executor --mode enforce <public key> and sends you back the key_id that command prints: that is your bot key id. The mode in the id (enforce, shadow or test) is the mode of every order the key asks about. Keep executor.pem secret: anyone holding it can ask for decisions as your agent.

Configure

import retrade

rt = retrade.Client(
    "exk_enforce_01M...",            # your bot key id
    "executor.pem",                  # your executor key file
    root_key="<the root public key>",
    base_url="http://127.0.0.1:8080",
)

The server's address comes from, in this order: the base_url argument, then the RETRADE_BASE_URL environment variable, then retrade.DEFAULT_BASE_URL (https://api.retrade.app, the hosted service). So you can leave base_url out and run with RETRADE_BASE_URL=http://127.0.0.1:8080. Plain http is allowed only for a loopback address (127.0.0.1, localhost, ::1). The older keyword server= still works.

The root key is pinned: the SDK never fetches it. On start-up it fetches reTrade's list of signing keys and checks the list against the root key. The claim kit (below) goes in state_dir, retrade-state by default.

Connect your broker

The SDK reads the account and places orders through four functions you write against your broker's own API:

from datetime import UTC, datetime
from decimal import Decimal


def read_account(instrument, adjustments_since):
    """Read the account from your broker now, and a quote for `instrument` when it is given."""
    now = datetime.now(UTC)
    snapshot = retrade.Snapshot(
        as_of=now,
        cash={"USD": Decimal("38380.90")},          # from your broker
        settled_cash={"USD": Decimal("38380.90")},  # from your broker
        positions=(retrade.Position("XNAS:AAPL", 81, Decimal("187.10")),),
        open_orders=(),
    )
    if instrument is None:
        return snapshot, None
    return snapshot, retrade.Quote(instrument, Decimal("187.10"), Decimal("187.08"), Decimal("187.12"), now)


broker = retrade.GenericAdapter(
    snapshot=read_account,
    submit=place_order,   # (retrade.ApprovedOrder, tag) -> the broker's order id
    find=find_order,      # (tag, retrade.ApprovedOrder, since) -> the broker's order id, or None
    cancel=cancel_order,  # (broker order id) -> None
    g_seconds=60,         # must equal the account's g_seconds at reTrade
    converts_cash=True,   # must equal the account's converts_cash at reTrade
)

submit places exactly the ApprovedOrder it is given, with tag in the broker's client-order-id field. It raises retrade.BrokerRejected only when the broker definitely refused the order, and lets any other error through when the outcome is unknown. The SDK never retries a submit.

Submit an order

from retrade import OrderType, Side, Tif

order = retrade.Order("XNAS:AAPL", Side.BUY, 40, OrderType.LIMIT, Tif.DAY, limit_price=Decimal("187.25"))
result = rt.place(order, account="A1", agent="agt_momentum", via=broker)

account is the account's ref in your tenant's account register. agent names the agent sending the order. Instruments are <MIC>:<symbol>, such as XNAS:AAPL. Plain strings ("buy", "limit", "day") and lists work too; the enums and tuples are what a type checker accepts. The package ships its type hints (py.typed).

Handle ALLOW, CUT and BLOCK

if result.outcome == "ALLOW":
    print("placed as sent:", result.broker_order_id)
elif result.outcome == "CUT":
    print(f"placed {result.quantity} of {result.requested_quantity}:", result.deciding)
elif result.fits:
    # BLOCK at an approval line or an agent conflict, and a smaller order would pass. The resend is a new
    # decision and counts toward the agent's per-minute limit, so resend once, don't loop.
    smaller = retrade.Order("XNAS:AAPL", Side.BUY, result.fits, OrderType.LIMIT, Tif.DAY, limit_price=Decimal("187.25"))
    result = rt.place(smaller, account="A1", agent="agt_momentum", via=broker)
else:
    # BLOCK, nothing placed: reTrade's rule, or the SDK's own reason when it had no verified answer.
    print("blocked:", result.deciding or result.reason)
  • Check result.placement as well: placed, not_placed (the broker refused it, or it was never sent), or unknown (the broker call's outcome isn't known yet; don't place the order again).
  • result.deciding is reTrade's deciding rule, for example {"code": "approval_line", "reason": "per_order"} or {"code": "order_size_cap"}.
  • result.fits is set only on a BLOCK at an approval line or an agent conflict. It is 0 when no size would pass.
  • result.local is True when the SDK decided without a verified answer from reTrade, with result.reason saying why: for example unreachable (no answer within 2.5 s) or unverified (the answer failed a check). In enforce and test mode the order is then not placed.

Verify receipts

The SDK checks every signed answer before it uses it:

  • the key list must be signed by the root key you pinned;
  • each decision must be signed by a listed key of the right environment, valid at the decision's time, and must answer the exact request bytes the SDK sent, with the same order fields, signed within 60 s of your clock;
  • after the broker call, the SDK reports what happened and checks the signed receipt reTrade sends back.

An answer that fails any check is never placed (result.reason == "unverified"). Everything is kept in retrade-state/claim-kit.sqlite3: the request bytes, the signed decision, the policy it was decided under, your report and reTrade's receipt. To read the evidence for one decision:

if result.decision_id is not None:  # None only when the SDK decided on its own
    evidence = rt.claim_kit(result.decision_id)
# {"format": "retrade-claim-kit/1", "decision_id": ..., "request": ..., "decision": ..., "policy": ...,
#  "report": ..., "receipt": ..., "heads": [...]}

or, on the command line, retrade claim-kit <decision id> --state-dir retrade-state. decision and receipt are compact JWS documents (Ed25519). Anyone with the root public key can check them again against GET /v1/keys, as API.md sections 3 and 4 describe. Keep the file for at least 90 days. The TypeScript SDK reads and writes the same format.

To read your tenant's log for this key's mode, each record verified and chained to the one before it:

records = rt.log()  # the whole log, from record 1: a list of retrade.LogRecord (seq, kind, hash, token, payload)
first20 = rt.log(from_seq=1, to_seq=20)  # records 1 to 20

reTrade sends the log a page at a time (at most 1,000 records and 512 KiB a page), and log() asks for one page after another until it reaches to_seq or the end of the log. Each page has its own 2.5 s deadline. The records it returns are held in memory, so read a very long log in ranges.

reTrade offers no guarantee and pays no losses. The signed records are evidence of what was asked, what reTrade answered and what was placed, nothing more.

Status

  • In development. 0.x versions may change the API between minor releases.
  • Wire spec: retrade-arith/2.0-draft.
  • The default address is the hosted service at https://api.retrade.app; pass base_url to use your own server.
  • Not built on the server yet: snapshots sent with no order (keep_fresh, send_snapshot; the server answers 404, which the SDK only logs) and webhooks.

Reference

What place returns

retrade.Result, with these fields:

Field Meaning
outcome ALLOW, CUT or BLOCK (retrade.Outcome)
placement placed, not_placed or unknown (retrade.Placement); unknown means the broker call's outcome isn't known yet, so don't place the order again
quantity the approved quantity (ALLOW, CUT), or what the SDK placed on its own; None for a BLOCK
requested_quantity the order's quantity
deciding reTrade's deciding rule, as reTrade sent it, for example {"code": "approval_line", "reason": "per_order"}; None for an ALLOW and for every result the SDK made itself
fits on a BLOCK at an approval line or an agent conflict, the largest quantity that would pass (0 when none would); otherwise None
flags the approval-line and agent-conflict codes reTrade listed on an ALLOW or CUT; empty while those always BLOCK
alerts reTrade's alerts, passed through
reason the SDK's own reason when it acted without or against an answer (below)
decision_id, broker_order_id the decision and the broker's order id
local True when the SDK decided without a verified answer from reTrade
claimable True when the decision was made in enforce mode and the order was placed; it marks evidence only, never a promise of payment

The SDK's own reasons: unreachable (no answer in 2.5 s), unverified (the answer failed a check, including one whose order fields differ from what was sent), exits-only:<why> (exits-only refused the sell), exit (exits-only placed it), shadow-unreachable and shadow-unverified, expired and stopped (an approved order withdrawn before the broker call), already-used, broker-rejected and broker-outcome-unknown.

Other calls

  • rt.replace(broker_order_id, order, account=..., agent=..., via=...): cancel a working order, then decide on the new one.
  • rt.stop(scope, account=None, agent=None, reason=...): engage a stop (tenant, account or agent). Any executor key can stop; only an admin key releases it.
  • rt.cancel_open_orders(via, all_orders=False), rt.send_pending_reports(), rt.refresh(), rt.send_snapshot(...), rt.keep_fresh(...), rt.log(from_seq=None, to_seq=None), rt.claim_kit(decision_id) and rt.close(). A Client is also a context manager.
  • rt.keep_fresh({"A1": broker}), every 5 minutes on a background thread until rt.close(): refreshes stops and account settings, and sends each account's snapshot.
  • retrade claim-kit DECISION_ID --state-dir DIR prints the evidence for one decision.

What the SDK does on your side

  • Nothing reaches your broker that reTrade didn't approve (in enforce and test mode), and never more than the approved quantity. A decision is placed at most once.
  • If reTrade doesn't answer within 2.5 s, the order is blocked (or, with exits-only on, a qualifying sell goes ahead).
  • reTrade's status (stops and account settings, exits-only included) is used only after its signature and freshness are checked.
  • In shadow mode, your agent's orders are placed unchanged; reTrade only records what it would have done.
  • The dollar lines are your tenant's policy, never constants in the SDK. A new tenant starts with an approval line of 5,000 per order and 10,000 per day, which its admins change with retrade-admin policy set. The one fixed number is exits-only's fallback cap: when the last rules this SDK decided under have no approval_line, an exits-only sell is held to 5,000 per order in the account's currency.

Exits-only, ids and submits

  1. Exits-only (an admin turns it on; it applies only while reTrade can't be reached): a limit sell of shares the broker holds now, at most held minus sells already working and this SDK's own recent sells the broker may not show yet (its exits, and sells reTrade approved), no more than 10% below last, of no company on the won't-sell list, within the per-order approval line (5,000 when the last rules have none), and with no stop known for the account or the agent. Everything else is refused.
  2. Ids and tags: a decision id is a ULID; the broker tag is rt1-<id> (enforce), rs1-<id> (shadow) or rx1-<id> (test), 30 characters. A broker whose client-order-id holds fewer characters would need a shorter form.
  3. A submit is never retried. When the outcome is unknown, the SDK looks the order up by its tag; if it still can't tell, the decision keeps counting at reTrade until a snapshot shows the order or its absence.

The wire formats are in API.md.

Development

uv sync
uv run python -m pytest -q
uv run ruff check .
uv run mypy

The server end-to-end test (tests/test_server_e2e.py) is skipped unless a server is running; the Rust test server/tests/sdk_e2e.rs starts one and runs it.

License

MIT. See LICENSE.

Metadata

Release files for retrade 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for retrade 0.1.1
File Size Uploaded Provenance Actions
retrade-0.1.1.tar.gz 40.8 kB Without provenance Details for retrade-0.1.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for retrade 0.1.1
File Interpreter ABI Platform Actions
retrade-0.1.1-py3-none-any.whl Python 3 none any Details for retrade-0.1.1-py3-none-any.whl

Total release size: 87.2 kB

Release files / retrade-0.1.1.tar.gz

Download URL retrade-0.1.1.tar.gz
Size 40.8 kB
Tags Source
SHA-256 checksum
How to use checksums
c970603aa62596795a4ba7d99527dad595df5838c5d276f3aac5613765755fa2
BLAKE2b-256 checksum
How to use checksums
b7e647a26101f56152978088c78b65e506679e5b4ee291ef01c86660275f3c71
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / retrade-0.1.1-py3-none-any.whl

Download URL retrade-0.1.1-py3-none-any.whl
Size 46.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7a885d97a64e217376e93dba882c9925bdc2f1925ad515c747ae4799bf115432
BLAKE2b-256 checksum
How to use checksums
8d646a364cd2454b97f5867fd8c61b1d7a277c561549d60dea6dcc61d8683bbe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Security for retrade 0.1.1

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page