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.placementas well:placed,not_placed(the broker refused it, or it was never sent), orunknown(the broker call's outcome isn't known yet; don't place the order again). result.decidingis reTrade's deciding rule, for example{"code": "approval_line", "reason": "per_order"}or{"code": "order_size_cap"}.result.fitsis set only on a BLOCK at an approval line or an agent conflict. It is 0 when no size would pass.result.localis True when the SDK decided without a verified answer from reTrade, withresult.reasonsaying why: for exampleunreachable(no answer within 2.5 s) orunverified(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; passbase_urlto 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,accountoragent). 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)andrt.close(). AClientis also a context manager.rt.keep_fresh({"A1": broker}), every 5 minutes on a background thread untilrt.close(): refreshes stops and account settings, and sends each account's snapshot.retrade claim-kit DECISION_ID --state-dir DIRprints 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 noapproval_line, an exits-only sell is held to 5,000 per order in the account's currency.
Exits-only, ids and submits
- 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.
- Ids and tags: a decision id is a ULID; the broker tag is
rt1-<id>(enforce),rs1-<id>(shadow) orrx1-<id>(test), 30 characters. A broker whose client-order-id holds fewer characters would need a shorter form. - 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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.