Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
# Copy this file to `.env` and fill in your own values.
# `.env` is gitignored - never commit real credentials.
#
# NOTE: none of these settings have been validated against a live DXtrade
# server. See the "Project Status" section of the README before spending time
# here. FTMO in particular disabled DXtrade REST access for clients in April
# 2024, so no combination of values below will work against FTMO.
# The client follows the public DXtrade spec but has not been verified against a
# live broker. Use a DEMO account. FTMO disabled DXtrade REST access for clients
# in April 2024, so no values below will work against FTMO.

# Base URL of your broker's DXtrade deployment (no trailing slash).
DXTRADE_BASE_URL=https://dxtrade.your-broker.com

# Credentials for the trading account.
# Credentials. The password is never logged by the client.
DXTRADE_USERNAME=
DXTRADE_PASSWORD=

# The broker's domain/vendor identifier, sent alongside the credentials.
# Your DXtrade domain, sent with the credentials (usually "default").
DXTRADE_DOMAIN_VENDOR=default

# REST prefix for the DXtrade SCA API. Every REST path is built under this.
# REST prefix; "/dxsca-web" per the DXtrade REST spec.
DXTRADE_API_PREFIX=/dxsca-web

# Login path, relative to DXTRADE_API_PREFIX.
DXTRADE_LOGIN_PATH=/login

# Account code / id. The login response does not reliably contain one, and the
# wrapper now raises instead of guessing, so set it explicitly if you have it.
# Account code in "clearing:account" form, e.g. default:ACC12345. Optional: if
# empty it is looked up via GET /users/{username}; required if you have several.
DXTRADE_ACCOUNT=

# WebSocket path. Unverified - differs between broker deployments.
DXTRADE_WEBSOCKET_PATH=/websocket/events
# Push API websocket URL. The spec leaves this to each deployment - ask your
# broker. Leave empty to skip streaming in example.py.
DXTRADE_WEBSOCKET_URL=

# Symbols example.py streams quotes for.
DXTRADE_SYMBOLS=EUR/USD,GBP/USD

# Older (0.1) settings, still read by example.py: login path relative to the API
# prefix, and the websocket path on the host (used when DXTRADE_WEBSOCKET_URL is empty).
# DXTRADE_LOGIN_PATH=/login
# DXTRADE_WEBSOCKET_PATH=/websocket
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.11", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -e ".[test]" ruff
- run: ruff check .
- run: python -m pytest -q

spec-conformance:
# Downloads Devexperts' public OpenAPI document (read-only) and checks the
# client's requests and the test fixtures against it.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install -e ".[test]" mypy types-requests
- run: mypy dxtrade_wrapper
- run: DXTRADE_SPEC_TESTS=1 python -m pytest tests/test_spec_conformance.py -v

package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install build && python -m build
- run: pip install dist/*.whl && cd /tmp && python -c "import dxtrade_wrapper; print(dxtrade_wrapper.__version__)"
62 changes: 62 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Changelog

## 0.2.0 (unreleased)

Rebuilt against the public DXtrade REST/Push specification
(https://demo.dx.trade/developers/, OpenAPI at /dxsca-web/swagger/openapi.json).
Spec-conformant; not yet verified against a live broker.

### Fixed in independent review
- `modify_order` on the entry (or a pending SL/TP) of an IF-THEN group sent a single
order, which per the spec turns the group into a single order and drops its stop
loss and take profit. Group members are now modified as the whole group; OCO members
and groups with a missing child are refused with nothing sent.
- Modify requests carried `positionCode` on OPEN orders (the order list includes it;
the spec says it must be omitted unless CLOSE).
- A 412 retry resent the body built from the stale order; it is now rebuilt from the
re-read order.
- A 5xx on order placement, or a group acknowledged with fewer order responses than
orders sent, is now `ambiguous=True`.
- Two threads hitting 401 on the same expired token no longer both log in.
- Build requirement raised to setuptools>=77 (needed for the SPDX `license` string).

### Fixed (each would have failed at any broker)
- Stop loss / take profit were never placed: they waited for a `positionCode` the
order response does not contain. Now one atomic IF-THEN order group.
- `get_balance()` read `/portfolio` for equity/margin (they are in `/metrics`) and
returned all-zero balances on any parse error. Now `/metrics`, and errors raise.
- Position/order parsers read invented fields (`id`, `entryPrice`, `stopLoss`...)
from a bare list; responses are `{"positions": [...]}` etc. with `positionCode`,
`symbol`, `openPrice`, `stopLossPrice`, `legs[0].price`...
- Order history used `/accounts/{a}/history`; the endpoint is `/orders/history`.
- `modify_order` sent `PATCH /orders/{id}` with `price`/`qty`; the spec requires
`PUT /accounts/{a}/orders` with the full order and `If-Match`.
- `cancel_order` sent no `If-Match` (403 "Conditional request required").
- `close_position` / `modify_position_sl_tp` called `/positions/{id}`, which does not
exist; closing is a linked CLOSE order, protections are linked STOP/LIMIT orders.
- `ping()` used GET; the spec says POST. A renewed `sessionToken` is now adopted.
- Push API: replaced invented `subscribe` messages, Atmosphere headers and
token-in-URL (logged at INFO) with the spec envelope, `PingRequest`→`Ping`
replies, a separate `/md` market-data channel, and reconnects that actually
replay subscriptions.

### Added
- Typed exceptions with `status_code`, `error_code`, `description`
(`NotFoundError`, `ConflictError`, `PreconditionFailedError`, `RateLimitError`,
`ServerError`); `OrderPlacementError.ambiguous` / `.order_code`.
- One automatic re-login on 401; keepalive capped by the server's session timeout.
- Account discovery via `GET /users/{login@domain}`; `get_accounts()`,
`get_account_metrics()`, `get_portfolio()`, `close_all()` (bulk close).
- `get_positions(include_pnl=True)` (floating P/L from per-position metrics);
one automatic retry of rate-limited (429) GETs.
- Context manager, `DXTradeClient` alias, `pyproject.toml`, `py.typed`.
- Conformance tests against the official OpenAPI document; Push tests over a real
local websocket; offline demo.

### Changed
- Module moved into the `dxtrade_wrapper` package (imports unchanged).
- The library no longer attaches log handlers or sets log levels.
- `Order.order_id` is the server `orderCode`; the client id is `client_order_id`.

## 0.1.0 (2025-04)
- Initial reverse-engineered prototype (never completed a call).
Loading
Loading