qjtrader
Hosted assistants should use the delegated OAuth connector from QJ Gateway, which keeps the trading client secret out of chat. This SDK remains the full programmatic client for Python services and local agents.
Official Python client for the QJ Trader AI Trading APIs — stream real-time Canadian and selected US market data and send orders through entitled Canadian or US gateway accounts (Montréal Exchange derivatives, and equities across every lit exchange and dark pool) over one authenticated connection.
pip install qjtrader
- Self-serve synthetic sandbox. Create an account at gateway.qjtrader.ai,
click Create sandbox credential, and you get a
client_id+client_secretthat stream simulated data and return simulated fills — in the exact production wire format, 24/7. Paid Production service is contracted and entitled separately. - Sandbox → production without a code change. Request licensed data and order authority independently. A human admin approves the scope, then separately provisions a dedicated least-privilege key; the SDK and sandbox credential cannot self-promote.
- Stdlib only. No dependencies — easy to install, easy to audit.
- Verifiable releases. Published straight from this repo via PyPI Trusted Publishing with signed PEP 740 provenance — no manual uploads, no stored tokens. See SECURITY.md to verify a release.
Quickstart
Get a sandbox key from the console, then:
export QJ_CLIENT_ID="your-client-id"
export QJ_CLIENT_SECRET="your-client-secret"
For a long-running service or coding agent, keep the dedicated machine credential outside the repository in an ACL-restricted file:
QJ_CLIENT_ID=your-dedicated-client-id
QJ_CLIENT_SECRET=your-dedicated-client-secret
client = qjtrader.Client.from_env_file("~/.qj/m3alpha-csu.env")
chmod 600 ~/.qj/m3alpha-csu.env
qjtrader subscribe CA:CSU --all-venues --depth 10 --watch 30 \
--env-file ~/.qj/m3alpha-csu.env
The SDK parses this file itself: it does not source shell expressions or copy secrets into the process-wide environment. On Windows, restrict the file to your user with NTFS permissions. Never store a Gateway password, MFA code, or human admin session in this file; it is only for a dedicated OAuth machine credential. Production data and order credentials still require separate human approval, and production order entry additionally requires an admin-selected existing trader profile.
Send an order
import qjtrader
client = qjtrader.Client() # reads QJ_CLIENT_ID / QJ_CLIENT_SECRET from the environment
with client.orders() as oe:
fill = oe.order_and_wait(
sym="MX:CRAU26", side="buy", qty=1, price=97.00, account="SIM", tif="ioc",
)
print(fill) # {'type': 'exec', 'status': 'filled', 'last_px': 97.0, 'cum_qty': 1, ...}
Lower-level, if you want every message:
with client.orders() as oe:
cid = oe.order(sym="MX:CRAU26", side="buy", qty=1, price=97.00, account="SIM")
for msg in oe.updates(timeout=10):
print(msg) # accepted -> new -> (partial)* -> filled | canceled | replaced
oe.cancel(cid)
print(oe.status()) # open orders + session state
qjtrader run supplies structured strategy, version, run, agent, and session identity on every
order. Generated cid values remain unique across later runs and reconnects: a cid is a forever
idempotency key, not a strategy-grouping convention. Execution events expose individual partial
fills with stable execution identity, account, venue, and broker timestamps when supplied.
For collision-safe journal paging, pass next_cursor from client.events() back using its
cursor= argument. The timestamp cursor/since pair remains compatible with older clients.
Use client.executions() for the trade-log projection: partial fills stay separate and later
broker cancel/correct adjustments remain visible without parsing every order transition.
Stream market data
import qjtrader
client = qjtrader.Client()
with client.market_data() as md:
md.subscribe(["CA:RY", "CA:RY.PT", "MX:CRAU26", "US:@ESU26"], depth=5)
for msg in md.messages(timeout=30):
if msg["type"] == "quote":
print(msg["symbol"], msg["data"]["bid"], msg["data"]["ask"])
CA:RYis the consolidated Canadian equity book (each level tagged with its venue);CA:RY.PTis PURE (CSE) only. Futures likeMX:CRAU26and selected US contracts such asUS:@ESU26are venue-native. Production access remains product- and entitlement-specific. See the full symbology reference.- On real consolidated Canadian symbols,
md.quote("CA:RY")waits for the officialcbbo=truequote. Canadian L2 carries five-level price views plus entitled QJ/TMX order-level TL2 rows inorder_bids/order_asks, including available venue, broker, order identity, and attributes.bids/asksare the rounded Top5 book; additiveodd_lot_bids/odd_lot_asksandspecial_lot_bids/special_lot_asksexpose full displayed sizes by desktop book type and must not be summed into Top5. - Treat message provenance as a safety input. Reject or quarantine
meta.stale=true; usemeta.transport_age_msto measure agent-to-gateway delay andmeta.stale_reason == "transport_backlog"to distinguish a delayed uplink from a disconnected source. An initial cached book carriesmeta.cached_snapshot=trueandmeta.snapshot_age_ms. For an exact venue-scoped TL2 book, that venue's healthy heartbeat can reaffirm unchanged state, so this is time since the last exact-venue change or reaffirmation;transport_age_msdescribes relay speed for that observation.
Check what is available
Coverage differs by product and entitlement, especially for US depth. The offline matrix requires no credential or network connection:
from qjtrader import market_availability
print(market_availability()["markets"]["US"])
Verified examples include AAPL L1, SPY L1/L2, ES futures L1/L2, and Treasury futures
US:@USU26 (30-Year Bond), US:@TYU26 (10-Year Note), and US:@FVU26 (5-Year Note).
AAPL depth, NDX,
and US listed-option depth are not currently available. See
Market Availability.
History is provenance-safe: sandbox responses say source="synthetic"; production responses are
recorded or unavailable. An empty not_recorded range is not replaced with generated bars.
Production market memory follows actual attention. QJ captures lightweight bars while a user or agent observes a symbol. Pin only the important markets that should keep recording after every app disconnects:
client.recording_status("CA:RY")
client.pin_recording("CA:RY") # continuous memory; richer market events
client.unpin_recording("CA:RY") # returns to observation-driven recording
Read recorded sub-second quotes, trades and Level 2 snapshots after pinning:
client.pin_recording("MX:CGBU26")
events = client.market_events(
"MX:CGBU26",
frm="2026-07-31T14:59:00-04:00",
to="2026-07-31T15:00:00-04:00",
types=["trade", "quote", "level2"],
)
# Normalized SXF Basis Trade on Close history. This joins BSF basis trades to
# finalized SXF allocations and corrections; it is not an auction imbalance.
close = client.sxf_close_flow(
frm="2026-08-04T00:00:00Z", to="2026-08-05T00:00:00Z"
)
# True TSX/TSXV MOC auction history: authoritative TL1 updates plus the
# aggregated closing auction. Raw transport copies remain in market_events().
auction = client.auction_imbalance(
"CA:RY", frm="2026-08-04T19:50:00Z", to="2026-08-04T20:01:00Z"
)
# Canadian rates/index-futures settlement history.
settlements = client.settlement_flow(
"CGB", frm="2026-08-06T00:00:00Z", to="2026-08-07T00:00:00Z"
)
MX does not disseminate a CGB/SXF auction-imbalance message. For SXF Basis Trade
on Close flow, record the separate MX:BSF<month><year> book alongside the
ordinary MX:SXF<month><year> contract.
Command line
The package installs a qjtrader command:
qjtrader init my-strategy --symbol MX:CRAU26
qjtrader login # browser sign-in; separate from trading API keys
qjtrader access-status
qjtrader access-request --plane data --market ca-equities
qjtrader limit-request --product us-futures --max-qty 2 --daily-qty 40 --reason "two-leg strategy"
qjtrader access-admin-list
qjtrader access-admin-decide __prodreq__... approved --market ca-equities # omit --market to approve the requested set
qjtrader access-admin-apply __prodreq__... \
--trader-profile M3ALPHA --account 12345 \
--max-order-qty 99 --max-open-orders 12 \
--max-messages-per-second 25 --max-daily-qty 5000
# Dedicated data-admin credentials can inspect or change per-user feed quotas.
# The feed enforces the admin scope; a trading/data key cannot self-elevate.
qjtrader feed-admin-limits strategy-client --env-file ~/.qj/admin.env
qjtrader feed-admin-limits strategy-client --max-symbols 250 --env-file ~/.qj/admin.env
qjtrader subscribe CA:RY MX:CRAU26 US:@ESU26 --watch 30 --env-file ~/.qj/strategy.env
qjtrader data-doctor CA:RY --all-venues --duration 30 --env-file ~/.qj/strategy.env
qjtrader order --sym MX:CRAU26 --side buy --qty 1 --price 97.00 --account SIM --tif ioc
qjtrader status
qjtrader cancel --orig qj-abc123
# strategies: the same file runs in backtest and live
qjtrader backtest examples/strategy_meanreversion.py --symbol MX:CRAU26 --bars 200
qjtrader run examples/strategy_meanreversion.py --symbols MX:CRAU26 --tag mr1
qjtrader runs
qjtrader stop-run local-abc123
qjtrader init creates a small local project that observes by default and keeps order mutation
disabled until the user deliberately changes allow_orders. It is designed for a coding agent to
inspect, test, and run locally without adding a cloud IDE or another account-setup step.
Strategies — one contract, every venue
Subclass Strategy and the same file runs in the backtest engine, a paper run, or
live (plan §10). Backtests are offline and deterministic (no network, no secrets);
qjtrader run hosts it against a live/paper credential, tags every order with the
strategy name, writes a local run record, and cancels working orders on Ctrl-C or
qjtrader stop-run RUN_ID. Access commands use browser-authenticated human identity;
ordinary trading credentials cannot approve or provision themselves.
Access is account-level, while API keys are revocable delegations. Most users need one production key and one sandbox key. A production key may use all or a restricted subset of the account's approved Data markets, Order Entry markets, and linked trading accounts; creating or restricting a key never grants a new market or account.
from qjtrader import Strategy, run_backtest, synthetic_bars
class Buy2Percent(Strategy):
def on_bar(self, ctx, bar):
if ctx.position(bar["symbol"]) == 0 and bar["close"] < ctx.param("floor", 0):
ctx.buy(bar["symbol"], 1, bar["close"], tif="ioc")
def on_fill(self, ctx, fill):
ctx.log("filled", fill.get("cid"), "@", fill.get("last_px") or fill.get("price"))
report = run_backtest(Buy2Percent(), synthetic_bars("MX:CRAU26", 200), params={"floor": 95})
print(report["total_pnl"], report["positions"])
The bar-level backtester is for logic; L2 event-replay with queue-model fills (microstructure truth) comes from the paper environment.
Configuration
Client() reads these (constructor args override environment):
| Setting | Env var | Default |
|---|---|---|
| Client ID | QJ_CLIENT_ID |
— (required) |
| Client secret | QJ_CLIENT_SECRET |
— (required) |
| Token endpoint | QJ_TOKEN_URL |
QJ Cognito token URL |
| Market-data host | QJ_DATA_HOST |
data-feed.qjtrader.ai:7000 |
| Order-entry host | QJ_ORDERS_HOST |
orders.qjtrader.ai:7001 |
| Pinned CA/cert | QJ_CA_FILE |
none (standard public-CA validation) |
Tokens are minted for you (OAuth2 client-credentials) and refreshed automatically before they
expire — you never handle them directly. Need a raw token (e.g. for the WebSocket interface)?
client.token(qjtrader.MARKET_DATA_SCOPE).
Use client.session_info() when a local agent needs the Gateway's authoritative Data and Order
Entry environments. The authenticated session also returns the market products and trading
accounts active on this particular key. These are a restricted subset of the human user's approved
access. For combined production keys, Order Entry also returns accounts by market product, and the
positions endpoint reports product-specific cloud limits alongside broker, fill, and total position
context where the OMS snapshot is available.
client.account_activity() returns the current account-wide working orders and retained
executions for those delegated accounts, including activity created by QJTrader Desktop or another
authorized route. This is distinct from client.order_snapshot() and client.executions(), which
are the calling credential's command journal. Unknown-origin orders are visible but intentionally
not cancelable, and execution_history_coverage reports the honest per-account retention boundary.
Gateway access; changing a local setting or requesting another OAuth scope cannot widen them.
client.search_universe() and client.describe_instrument(symbol) provide small,
machine-readable discovery helpers so code does not have to infer product identity from prose.
Both public API hosts use standard public-certificate validation. QJ_CA_FILE remains available
for controlled private deployments but is not required for the hosted QJ Gateway services.
How it works
Both APIs speak NDJSON over TLS — one JSON object per line, UTF-8, newline-terminated,
authenticated with an OAuth2 JWT sent on the first line. The order lifecycle is a deterministic,
journaled state machine (accepted → new → (partial)* → filled | canceled | replaced), commands are
idempotent per client order id (cid), and the server enforces pre-trade risk checks +
cancel-on-disconnect. Full protocol: Order Entry
and Market Data.
Use it from an LLM (MCP)
Prefer to drive QJ from Claude or another AI assistant? The companion
qjtrader-mcp server exposes these APIs as Model
Context Protocol tools — subscribe to quotes and place simulated orders in plain language, no
code. Order tools refuse a live credential by default (sandbox-only unless you opt in). Add it to
Claude Code with:
claude mcp add qjtrader -e QJ_CLIENT_ID=... -e QJ_CLIENT_SECRET=... -e QJ_ENV=sandbox -- uvx qjtrader-mcp
The console's "Connect your AI" panel generates this for you, pre-filled.
Links
- 📚 Docs: https://docs.qjtrader.ai/docs/ai
- 🔑 Console (get a key): https://gateway.qjtrader.ai
- 🔤 Symbology: https://docs.qjtrader.ai/docs/ai/symbology
- 🐛 Issues: https://github.com/QJTrader/qjtrader-python/issues
License
Apache-2.0. See LICENSE.
Metadata
Release files for qjtrader 0.5.26
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 | |
|---|---|---|---|---|
| qjtrader-0.5.26.tar.gz | 99.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| qjtrader-0.5.26-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 180.2 kB
Release files / qjtrader-0.5.26.tar.gz
| Download URL | qjtrader-0.5.26.tar.gz |
|---|---|
| Size | 99.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
82c6ee39f259d86e9296b901e133f10ecc7e4eb64b76ed9a1d333c94ea5f39f2
|
|
BLAKE2b-256 checksum How to use checksums |
8944c84f1ae2a6999ea7f3627221228a65233ee4f2e319aad871a332c2fd6455
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 7, 2026.
Transparency logRelease files / qjtrader-0.5.26-py3-none-any.whl
| Download URL | qjtrader-0.5.26-py3-none-any.whl |
|---|---|
| Size | 80.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ef46ded95260bb8923d111eac5544b5e27052f8b390f39bdcf35c6c5e290f1b7
|
|
BLAKE2b-256 checksum How to use checksums |
89b2961887a24366c791f49cb9a1e9cc371ac7bdbc63187c704a8ed7043fff8b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 7, 2026.
Transparency logSecurity for qjtrader 0.5.26
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for qjtrader-0.5.26.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 7, 2026.
Transparency logA 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.