Skip to content

Commit 643f1a5

Browse files
committed
darwinex prep, portfolioworth fix
1 parent 3718f7a commit 643f1a5

14 files changed

Lines changed: 854 additions & 11 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Darwinex DXtrade Live Trading Integration
2+
3+
## Context
4+
5+
The repo already supports three live brokers (Collective2, Interactive Brokers, eToro) via a shared `LiveBroker` abstraction in [tradingbot/livetrade/broker.py](tradingbot/livetrade/broker.py). The user just provisioned a Darwinex DXtrade CFD demo account (`dxtrade.darwinex.com`, master password auth) and wants a fourth broker added that mirrors the eToro/C2 wiring so existing bot weights can be copied to Darwinex via the same `LiveTradeCopier`.
6+
7+
User decisions (locked in via clarifying Qs):
8+
- **API flavor**: official `/dxsca-web` REST + WebSocket (session-token auth via username + password)
9+
- **Symbol mapping**: catalog search first, fall back to a manual `symbol_mappings.json` entry — Darwinex is CFD-only so equity tickers like `QQQ` may need `QQQ.US`-style codes
10+
- **Default `dry_run`**: `false` (live from first successful run, like Collective2)
11+
- **Credentials**: only DXtrade username + master password — no OAuth consumer key
12+
13+
Out of scope: DARWIN Info API (separate product), FIX API, MT4/MT5 paths.
14+
15+
## DXtrade `/dxsca-web` API surface (confirmed via Devexperts SDK + community refs)
16+
17+
- **Base URL**: `https://dxtrade.darwinex.com/dxsca-web`
18+
- **Login**: `POST /login` with `{"username", "password", "domain": "default"}` → returns session token in JSON; subsequent requests pass `Authorization: DXAPI <token>` header. Token must be refreshed on 401.
19+
- **Account/portfolio**: `GET /accounts/{accountId}/metrics` (cash, equity, margin) and `GET /accounts/{accountId}/portfolio` (positions list with `instrumentCode`, `quantity`, `openPrice`).
20+
- **Orders**: `POST /accounts/{accountId}/orders` with `{instrument, quantity, side: "BUY"|"SELL", type: "MARKET", legs: [...]}`. Close-by-position handled by submitting an opposing market order against `instrument` for the position quantity (DXtrade nets automatically).
21+
- **Cancel**: `DELETE /accounts/{accountId}/orders/{orderId}` (only relevant for non-market orders; we submit MARKET so cancellation is rarely needed).
22+
- **Instruments**: `GET /instruments?symbol=<query>` returns matching instrument descriptors (`symbol`, `description`, `type`, `currency`, etc.). Used for catalog search in `map_symbol`.
23+
- **Live quotes**: native quotes are WebSocket-only (`wss://dxtrade.darwinex.com/dxsca-web/md?format=JSON`). For v1 we **do not** open the WebSocket — `_get_native_price()` returns `0.0` and the base class falls back to yfinance, identical to how Collective2 behaves today. Worth revisiting once equities are confirmed CFD-mapped.
24+
25+
> All endpoint paths are best-effort from public docs / community SDKs and must be verified against the live `/dxsca-web/specs` Swagger on first run. The plan deliberately keeps each endpoint isolated in a `_get`/`_post` helper so any path correction is a one-line fix.
26+
27+
## Files to create
28+
29+
### 1. `tradingbot/livetrade/darwinex.py` — broker class
30+
Mirror [tradingbot/livetrade/etoro.py](tradingbot/livetrade/etoro.py) structure. Class `DarwinexBroker(LiveBroker)`:
31+
- `__init__(username, password, account_id=None, demo=True, symbol_mapper=None, data_service=None)` — store creds, build `httpx.Client(base_url=...)`, set `self.name = "darwinex"`.
32+
- `_login()` — POST `/login`, store `self._session_token` and `self._token_expires_at`.
33+
- `_ensure_session()` — called at the top of every `_get/_post`; re-login on 401 or near-expiry.
34+
- `_get(path, params)` / `_post(path, json_data)` / `_delete(path)` — auth header injection + `raise_for_status()`.
35+
- `_resolve_account_id()` — if `account_id` not supplied, GET `/users/{username}/accounts` and pick the first (mirrors how IB picks the configured account).
36+
- `get_cash()``/accounts/{id}/metrics``balance`.
37+
- `get_total_equity()``/accounts/{id}/metrics``equity`.
38+
- `get_positions()``/accounts/{id}/portfolio` → dict of `instrumentCode → signed quantity`. Cache `instrumentCode → positionId` if needed for closes.
39+
- `_get_native_price()` → return `0.0` (yfinance fallback). Leave a `# TODO: WebSocket md subscription for live quotes` line since this is a known v1 limitation, not dead code.
40+
- `place_order(broker_symbol, quantity, side, symbol_type)` → POST `/accounts/{id}/orders` with `{instrument: broker_symbol, quantity, side, type: "MARKET"}`. Respect `dry_run` upstream (copier handles it; broker doesn't need to know).
41+
- `map_symbol(yf_symbol)`**two-step** per the user's choice:
42+
1. Check `self.symbol_mapper.map_symbol(yf_symbol, "darwinex")` — if a manual mapping exists in `symbol_mappings.json`, return it.
43+
2. Otherwise `GET /instruments?symbol={yf_symbol}` and `{yf_symbol}.US` (covers the common equity-CFD naming). Return the first exact-match `{symbol, description, type}` dict; `None` if no match.
44+
- Cache results in `self._instrument_cache` to avoid hammering the search endpoint each sync.
45+
- `search_symbol(query)` → thin wrapper over `GET /instruments?symbol={query}`.
46+
- `cancel_open_orders()` → list `/accounts/{id}/orders?status=PENDING`, DELETE each, return count. Matches eToro behavior.
47+
- `print_account_summary()` → log cash/equity/positions (copy from C2 verbatim, swap field names).
48+
49+
### 2. `tradingbot/livetrade_darwinex.py` — runner/CLI entry
50+
Copy [tradingbot/livetrade_etoro.py](tradingbot/livetrade_etoro.py) and swap:
51+
- Env vars: `DARWINEX_USERNAME`, `DARWINEX_PASSWORD`, `DARWINEX_ACCOUNT_ID` (optional), `DARWINEX_DEMO` (default `"true"`).
52+
- Default for `LIVETRADE_DRY_RUN`: `"false"` (per user decision — matches C2 runner).
53+
- Keep the existing DB validation block from the eToro runner unchanged (validates `LIVETRADE_BOT_WEIGHTS` against the Bot table).
54+
- Instantiate `DarwinexBroker(...)`, then `LiveTradeCopier(...).sync()`.
55+
56+
### 3. `tests/test_livetrade_darwinex.py`
57+
Mirror [tests/test_livetrade_etoro.py](tests/test_livetrade_etoro.py). Mock `httpx.Client` at the broker; cover:
58+
- `_login` stores token from response
59+
- `get_cash` / `get_total_equity` parse the metrics response
60+
- `get_positions` aggregates portfolio response into `{symbol: qty}`
61+
- `map_symbol` (a) hits the manual map first, (b) falls back to instrument search, (c) returns None on no match
62+
- `place_order` posts the right JSON body for BUY and SELL
63+
64+
### 4. `helm/tradingbots/templates/cronjob-livetrade-darwinex.yaml`
65+
Copy `cronjob-livetrade-etoro.yaml`, swap broker name, env-var names, and gate by `.Values.liveTrade.darwinex.enabled`.
66+
67+
## Files to modify
68+
69+
- **`helm/tradingbots/values.yaml`** — add a `liveTrade.darwinex` block under the existing `liveTrade.etoro` block (`enabled: false`, `schedule`, `demo: true`, optional `portfolioFraction`, `accountId` optional).
70+
- **`README.md`** — add Darwinex to the supported-brokers list, the env-var quick-start block, and the copier/Helm flag table (alongside the existing eToro additions).
71+
- **`docs/guides/live-trading.md`** — add a Darwinex section parallel to the eToro one; extend the configuration reference table to include `DARWINEX_*` rows; update the alpha-notice line to include Darwinex.
72+
- **`AGENTS.md`** — add a Darwinex API quirks subsection under the existing eToro #10 quirks block (session-token auth, CFD-only catalog, no native price endpoint, two-step symbol resolution).
73+
- **`symbol_mappings.json`** — add a small starter map for the most common bot-universe tickers (e.g. `QQQ → QQQ.US`, `SPY → SPY.US`) so the very first sync isn't entirely catalog-search dependent. Concrete codes to be confirmed against `/instruments` on first connect.
74+
75+
## Verification (end-to-end)
76+
77+
1. **Unit tests**: `pytest tests/test_livetrade_darwinex.py -v` — all green with mocked httpx.
78+
2. **Live demo dry-run sanity**: temporarily export `LIVETRADE_DRY_RUN=true` and run `python -m tradingbot.livetrade_darwinex` against the demo. Expect: login OK → metrics fetched → equity > 0 → positions empty → bot weights resolved → mapping log shows which yf tickers map to which DXtrade instruments → orders printed but not sent.
79+
3. **Live demo wet run**: with `LIVETRADE_DRY_RUN=false`, confirm a single small BUY (e.g. `EUR/USD` with min order size) lands in the DXtrade UI.
80+
4. **Mapping coverage spot-check**: run a one-off helper invoking `broker.map_symbol("QQQ")`, `("AAPL")`, `("BTC-USD")`, `("EURUSD=X")` and log results — surfaces which of your active bot tickers are catalog-resolvable vs need manual `symbol_mappings.json` entries.
81+
5. **Lint/type**: whatever the repo runs in CI for the eToro module (no special needs here).
82+
83+
## Known v1 gaps (worth flagging in commit/PR)
84+
85+
- No WebSocket live quotes — equity calculation uses yfinance for position notional, fine for equities/crypto but inaccurate for FX/CFD-only instruments off-hours.
86+
- DXtrade nets positions automatically; the copier's SELL-then-BUY ordering still works but `get_positions()` returns the *net* position, not per-trade lots. eToro had the inverse problem (per-position lots). Should be a non-issue but worth noting.
87+
- Endpoint paths are best-effort from community SDKs; first live run will likely surface a 404 on one or two of them, easily corrected via `/dxsca-web/specs`.

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ ETORO_API_KEY=
4141
ETORO_USER_KEY=guestros
4242
ETORO_DEMO=true
4343

44+
# Darwinex DXtrade (https://dxtrade.darwinex.com/)
45+
DARWINEX_USERNAME=
46+
DARWINEX_PASSWORD=
47+
DARWINEX_ACCOUNT_ID=
48+
DARWINEX_DEMO=true
49+
4450
LIVETRADE_BOT_WEIGHTS={"AdaptiveMeanReversionBot": 1.0}
4551
LIVETRADE_COPY_OPEN_TRADES=true
4652
LIVETRADE_MIN_ORDER_USD=50

.vscode/launch.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,33 @@
5858
"PYTHONPATH": "${workspaceFolder}/tradingbot"
5959
}
6060
},
61+
{
62+
"name": "LiveTrade: Darwinex (paper)",
63+
"type": "debugpy",
64+
"request": "launch",
65+
"program": "${workspaceFolder}/tradingbot/livetrade_darwinex.py",
66+
"console": "integratedTerminal",
67+
"cwd": "${workspaceFolder}/tradingbot/",
68+
"envFile": "${workspaceFolder}/.env",
69+
"env": {
70+
"LIVETRADE_DRY_RUN": "false",
71+
"DARWINEX_DEMO": "true",
72+
"PYTHONPATH": "${workspaceFolder}/tradingbot"
73+
}
74+
},
75+
{
76+
"name": "Print Portfolio: Darwinex",
77+
"type": "debugpy",
78+
"request": "launch",
79+
"program": "${workspaceFolder}/tradingbot/livetrade/darwinex.py",
80+
"console": "integratedTerminal",
81+
"cwd": "${workspaceFolder}/tradingbot/",
82+
"envFile": "${workspaceFolder}/.env",
83+
"env": {
84+
"DARWINEX_DEMO": "true",
85+
"PYTHONPATH": "${workspaceFolder}/tradingbot"
86+
}
87+
},
6188
{
6289
"name": "Print Portfolio: eToro",
6390
"type": "debugpy",

AGENTS.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,35 @@ The eToro Public API (`https://public-api.etoro.com`, `/api/v1` prefix) has a nu
863863
- Body must include `{"InstrumentId": <int>}` even though the position ID is in the path. Missing instrument ID → HTTP 400 "InstrumentId: The instrument id does not exist".
864864
- httpx with `json=None` omits the body **and** the `Content-Type` header → eToro returns 415 Unsupported Media Type. Always pass at least `{}` (or the InstrumentId payload above).
865865

866-
### 11. `POSTGRES_URI` Required Even for Non-DB Tests
866+
### 11. Darwinex (DXtrade) Broker — API Quirks
867+
868+
The Darwinex DXtrade API (`/dxsca-web`) has its own set of nuances.
869+
870+
**Auth**: Session-token based.
871+
- `POST /login` with `{"username", "password", "domain": "default"}` returns `sessionToken`.
872+
- Subsequent requests require `Authorization: DXAPI <token>` header.
873+
- Tokens expire (typically 24h, but v1 implementation refreshes on 401 or after 2h).
874+
875+
**Account Selection**: A single user may have multiple accounts.
876+
- Use `GET /users/{username}/accounts` to list them.
877+
- If `DARWINEX_ACCOUNT_ID` is not provided, the broker picks the first one from this list.
878+
879+
**Portfolio & Metrics**:
880+
- Metrics (`GET /accounts/{id}/metrics`): Returns `balance` (cash) and `equity` (MTM value).
881+
- Portfolio (`GET /accounts/{id}/portfolio`): Returns a list of `positions`.
882+
- DXtrade nets positions automatically; `get_positions()` aggregates by `instrumentCode`.
883+
- Position quantity is signed based on `side` (BUY = positive, SELL = negative).
884+
885+
**Symbol Mapping (CFD Catalog)**:
886+
- Darwinex is a CFD-only broker. Equities are often suffixed with `.US` (e.g., `AAPL.US`).
887+
- `map_symbol()` first checks the manual `symbol_map.json`, then performs a catalog search (`GET /instruments?symbol=...`) for both the bare ticker and the `.US` variant.
888+
- Catalog search results can be a list or wrapped in an `instruments` key; implementation handles both.
889+
890+
**No Native Last Price**:
891+
- The REST API does not provide a simple snapshot "last price" endpoint. Quotes are WebSocket-only (`/md`).
892+
- `_get_native_price()` returns `0.0`, triggering the base class's **yfinance fallback** for all order sizing and equity calculations.
893+
894+
### 12. `POSTGRES_URI` Required Even for Non-DB Tests
867895

868896
**Problem**: `pytest tests/...` fails with `KeyError: 'Set POSTGRES_URI or (POSTGRES_HOST + POSTGRES_PASSWORD) for database connection'` even when running tests that don't touch the DB (e.g. pure-mock tests under `tests/test_livetrade.py`).
869897

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -377,12 +377,12 @@ telegramMonitor:
377377

378378
See [Telegram Monitor Guide](docs/guides/telegram-monitor.md) for full setup instructions.
379379

380-
## 📈 Live Trading (Collective2, Interactive Brokers & eToro)
380+
## 📈 Live Trading (Collective2, Interactive Brokers, eToro & Darwinex)
381381

382382
> [!WARNING]
383383
> **DISCLAIMER:** This software is for educational and research purposes only. Trading involves significant risk of loss and is not suitable for all investors. Use of "Live Trading" features is strictly at your own risk. The authors and contributors are not liable for any financial losses, damages, or unintended trades incurred. Always test strategies thoroughly in a paper-trading environment before deploying real capital.
384384

385-
The framework can mirror your paper-bot portfolios to a live brokerage account. Supported brokers: **Collective2** (World API v4), **Interactive Brokers** (via IB Gateway / `ib_async`), and **eToro** (Public REST API).
385+
The framework can mirror your paper-bot portfolios to a live brokerage account. Supported brokers: **Collective2** (World API v4), **Interactive Brokers** (via IB Gateway / `ib_async`), **eToro** (Public REST API), and **Darwinex** (DXtrade API).
386386

387387
### 1. Configure Environment
388388

@@ -404,6 +404,12 @@ ETORO_API_KEY=your_public_key
404404
ETORO_USER_KEY=your_user_key
405405
ETORO_DEMO=true # true for demo/paper, false for live
406406
407+
# Darwinex (DXtrade API)
408+
DARWINEX_USERNAME=your_username
409+
DARWINEX_PASSWORD=your_password
410+
DARWINEX_ACCOUNT_ID=12345 # optional
411+
DARWINEX_DEMO=true # true for demo/paper, false for live
412+
407413
# Shared
408414
LIVETRADE_BOT_WEIGHTS='{"adaptivemeanreversionbot": 1.0}'
409415
LIVETRADE_DRY_RUN=false
@@ -425,6 +431,9 @@ uv run python tradingbot/livetrade/interactive_brokers.py
425431
426432
# eToro (reads ETORO_API_KEY, ETORO_USER_KEY, ETORO_DEMO from .env)
427433
uv run python tradingbot/livetrade/etoro.py
434+
435+
# Darwinex (reads DARWINEX_USERNAME, DARWINEX_PASSWORD, DARWINEX_DEMO from .env)
436+
uv run python tradingbot/livetrade/darwinex.py
428437
```
429438

430439
### 2. Map Your Tickers
@@ -455,9 +464,12 @@ uv run python tradingbot/livetrade_interactive_brokers.py
455464
456465
# eToro
457466
uv run python tradingbot/livetrade_etoro.py
467+
468+
# Darwinex
469+
uv run python tradingbot/livetrade_darwinex.py
458470
```
459471

460-
Each broker is its own Helm CronJob gated by an independent flag in `values.yaml` — enable them separately (`liveTrade.enabled` for Collective2, `liveTradeIB.enabled` for IBKR, `liveTradeEToro.enabled` for eToro), so you can run any combination or none. All default to `false`. You can also cap how much of the account each broker mirrors via `LIVETRADE_PORTFOLIO_FRACTION` (default `1.0` = full account; e.g. `0.5` = half).
472+
Each broker is its own Helm CronJob gated by an independent flag in `values.yaml` — enable them separately (`liveTrade.collective2.enabled`, `liveTrade.interactiveBrokers.enabled`, `liveTrade.etoro.enabled`, `liveTrade.darwinex.enabled`), so you can run any combination or none. All default to `false`. You can also cap how much of the account each broker mirrors via `LIVETRADE_PORTFOLIO_FRACTION` (default `1.0` = full account; e.g. `0.5` = half).
461473

462474
See the [Live Trading Guide](docs/guides/live-trading.md) for advanced configuration and mapping rules.
463475

0 commit comments

Comments
 (0)