
Ctrader Mcp Servers
- 285 installs
- 5 repo stars
- Updated May 15, 2026
- spotware/ctrader-skills
Helps with ai & agent building tasks.
About
ctrader-mcp-servers is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- ctrader-mcp-servers
- AI & Agent Building
- AI-coding skill
Ctrader Mcp Servers by the numbers
- 285 all-time installs (skills.sh)
- +35 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #2,399 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spotware/ctrader-skills --skill ctrader-mcp-serversAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 285 |
|---|---|
| repo stars | ★ 5 |
| Last updated | May 15, 2026 |
| Repository | spotware/ctrader-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Top-5 critical quirks (inline teaser)
The following 5 quirks are the most consequential as of the audit date. Full Detect / Workaround / Verify-fixed / Removal-criteria content lives in references/known-quirks.md; the rows below are link-only teasers. See also references/self-healing-playbook.md for the named recovery patterns.
| Quirk | Server | One-liner | Link |
|---|---|---|---|
| Q-R10 | remote | amend_position omitting a leg REMOVES it (not preserves it) | Q-R10 |
| Q-R4 | remote | MARKET REJECTS absolute SL/TP — use relativeStopLoss/relativeTakeProfit (single call) or P-REMOTE-MARKET-2STEP (fallback) | Q-R4 |
| Q-R1 | remote | period enum is 9 values, NOT 26 (delete granular claims) | Q-R1 |
| Q-L2 | local | SL is absolute price, TP is raw pips (asymmetric get_pending_orders) | Q-L2 |
| Q-K19 | both | Pipettes vs display foot-gun — silent market fills | Q-K19 |
Self-healing principle
Both ctrader-remote-mcp and ctrader-local-mcp ship as-is with documented runtime behaviors last re-verified on 2026-05-14 against rest-proxy 1.0.18 (Remote) and local build observed-on 2026-05-14 (Local). Six Remote quirks remain ACTIVE on 1.0.18 (Q-R1, Q-R2, Q-R3, Q-R5, Q-R8, Q-R10). Two quirks had their error-message format refined (Q-R4 and Q-R7 now return plain-string error envelopes with actionable hints instead of JSON envelopes — both DETECT signatures cover both formats). One quirk is likely fixed (Q-R11 — get_deals propagation lag — passed Verify-fixed on session 1 of 5; not yet removed pending 4 more confirmations). This skill describes SEMANTICS, GOTCHAS, and RECOVERY; the MCP JSON-Schema is the source of truth on SHAPE. Never duplicate schema content in this skill. Every claim about runtime behavior in this skill carries a build-stamp — either an as-of: header on a reference file, or an Observed-on: line on a quirk in references/known-quirks.md.
Every quirk in the ledger is self-deprecating: it carries a Verify-fixed probe (a session-local check the agent can run) and a Removal criteria condition (the explicit signal that the server has been fixed and the entry should be deleted). When the server is fixed, delete the matching entry from references/known-quirks.md — that is the entire cleanup cost. Optional follow-up: grep for any stale QUIRK breadcrumbs across the reference files and remove them.
On unexpected server behavior (anything deviating from BOTH the MCP JSON-Schema AND the ledger), do NOT improvise — follow references/self-healing-playbook.md. The playbook names the pre-flight gates (quote sanity, side-direction, SL/TP sidedness, volume-step, schema-fields-only, pipettes-vs-display detection, required-fields), the post-flight verification rules (re-read after every mutation; for Remote amend_position, always re-read to confirm BOTH SL and TP legs survived per Q-R10), the error-classification matrix (Zod / INVALID_REQUEST / 502 uProxy / plain-text Local / available:false / truncated / hasMore), the unknown-quirk decision tree (4 steps from STOP to provisional ledger entry), and the named patterns (P-AMEND-SAFE, P-REMOTE-MARKET-RELATIVE (preferred for MARKET+SL/TP), P-REMOTE-MARKET-2STEP (fallback for absolute-price SL/TP on MARKET), P-REMOTE-MARKET-RANGE, P-LOCAL-OLDEST-FIRST, P-REMOTE-HISTORY-CHUNK).
Per-broker overlay extension point
The skill's quirks ledger uses the Q-B<n> prefix as a reserved slot for future per-broker overrides (lotSize divergences, symbol-naming variants, broker-specific SL/TP behaviors). A future assets/broker_overrides.example.json overlay would carry these per-broker values without restructuring this skill. NOT shipped this iteration — added on demand when broker-specific divergences become a blocking class.
Tool-surface routing: Local HTTP (ctrader-local-mcp) vs Remote HTTP (ctrader-remote-mcp)
The bound tool surface determines which cTrader MCP server is in front of you. Inspect tool names and response DTO shape to identify the family. Apply the routing rule below at the start of every cTrader-related interaction; cache the result for the session.
| Server family | Fingerprint to detect it | What it does best | Default routing rule |
|---|---|---|---|
Local HTTP (ctrader-local-mcp) | ping, get_accounts_list, list_charts, listChartIndicators, listPlugins, show_notification, get_server_time; volume in units; ISO 8601 with mandatory Z; symbol identified by string name ("EURUSD"). HTTP transport bound to the cTrader Desktop application. | Charts, drawings, indicators, watchlists, alerts, news, cBots, UI notifications, multi-account work. | Route here for any UI / visualization / cBot / chart-bound workflow. |
Remote HTTP (ctrader-remote-mcp) | get_version, get_assets, integer symbolId-keyed tools (get_spot_prices(symbolId), get_trendbars(symbolId)); moneyDigits field in money responses; volume in cents (1 lot of forex = 10 000 000); prices in pipettes. HTTP transport against the remote REST proxy (rest-proxy). | Headless trading, broad symbol scans, trailing stop loss, MARKET_RANGE, granular timeframe history. | Route here when no chart / UI need exists. |
| Both surfaces bound | Both fingerprints visible. | Depends on the action. | Pick by capability: drawings / cBots / charts go to Local; trailing SL / MARKET_RANGE / multi-symbol batch quotes / granular timeframes go to Remote. |
Units conventions across the two servers
The two servers encode the same trading concepts in different units and identifier types; passing Local values to Remote (or the reverse) silently produces wrong sizes, wrong prices, or schema rejections.
| Dimension | Local HTTP encoding | Remote HTTP encoding | Conversion script |
|---|---|---|---|
| Volume | Units (integer; for forex typically 1 lot = 100 000 units, but broker-dependent — always read get_symbol_details.lotSize). | Cents (integer; 1 lot of forex = 10 000 000 cents). | scripts/units_encoding.py lots-to-units / lots-to-cents |
| Price | Display value (e.g., 1.21345). | Pipettes (integer; divide by 10^pipDigits to display). | scripts/pip_math.py (handles both directions) |
| Money (balance, commission, swap, P&L) | Display value (e.g., 12345.67). | Integer in 10^moneyDigits units (moneyDigits field on the response; typically 2). | scripts/units_encoding.py display-money / parse-money |
| Timestamp | ISO 8601 with mandatory Z suffix (2026-01-15T14:30:00Z). | Epoch milliseconds for expirationTimestamp (integer-only as of rest-proxy 1.0.13); either form for history window endpoints. | Passed through unchanged for Local; check field type for Remote. |
| Symbol identifier | String name ("EURUSD", "XAUUSD"). | Integer symbolId (resolve via get_symbols, cache for the session). | N/A (look up by ID) |
Stop loss / take profit on place_*_order / amend_order | Pip distance integer (stopLossPips, takeProfitPips). | Absolute price (stopLoss, takeProfit). | scripts/pip_math.py pips-to-price / price-to-pips |
Stop loss and take profit: pip distance vs absolute price
Order-placement tools take SL / TP as pip distance from entry; position-amendment tools take SL / TP as absolute price. The convention depends on which TOOL is called, not which server.
| Tool | Server | SL / TP form |
|---|---|---|
place_market_order, place_limit_order, place_stop_order, place_stop_limit_order | Local | Pip distance (stopLossPips, takeProfitPips). |
amend_order (pending order) | Local | Pip distance (stopLossPips, takeProfitPips). |
amend_position (open position) | Local | Absolute price (stopLoss, takeProfit). |
create_order | Remote | Absolute price (stopLoss, takeProfit). See references/remote-http-server.md for MARKET-order SL/TP rejection (P-REMOTE-MARKET-2STEP). |
amend_order | Remote | Absolute price (stopLoss, takeProfit). |
amend_position | Remote | Absolute price (stopLoss, takeProfit). |
close_position | Both | N/A (no SL / TP parameter). |
When the user says "SL 30 pips below entry", convert to the form the target tool needs by running scripts/pip_math.py. Input: --pip-size <float> --digits <int> --reference-price <float> --pips <int>. Output: {"absolute_price": <float>, "pip_size_used": <float>}. The script handles both directions.
Dynamic-leverage margin calculation
Brokers apply dynamic-leverage tiers: leverage falls as the exposure grows past tier upper bounds. Required margin is computed PER TIER and summed. Tier exposure volumes are stated in USD regardless of the traded symbol; the resulting margin in USD is then converted to the account currency.
Example. Account currency USD. Order: 1 000 000 EURUSD long @ 1.21345. Notional in USD = 1 000 000 × 1.21345 = 1 213 450 USD. Tier curve: 1:500 up to 1 000 000 USD, 1:200 from 1 000 000 to 5 000 000 USD, 1:100 above 5 000 000 USD. Margin = (1 000 000 / 500) + (213 450 / 200) = 2 000 + 1 067.25 = 3 067.25 USD.
Invoke scripts/tiered_margin.py compute to compute this for arbitrary tier curves. Input: --volume-base-units <int> --quote-rate-usd <float> --tiers '[{"upper":1000000,"leverage":500},{"upper":5000000,"leverage":200},{"upper":null,"leverage":100}]'. Output: {"margin_usd": <float>, "per_tier_breakdown": [...]}. Convert the USD margin to the account currency with scripts/conversion_rate.py compute-chain.
Currency conversion: quote currency vs account currency
When a symbol's quote currency differs from the account currency, every money figure returned by the server (commission, swap, realized P&L, pip value, margin) requires conversion through a chain of spot rates. Example chains: P&L on USDJPY for a EUR account requires JPY -> USD -> EUR (using USDJPY and EURUSD); P&L on AUDCAD for an NZD account requires CAD -> USD -> NZD or a direct AUD -> NZD chain. The cTrader backend builds the shortest available chain; replicate that logic locally with scripts/conversion_rate.py.
Invoke scripts/conversion_rate.py compute-chain to derive the rate. Input: --from-asset <CCY> --to-asset <CCY> --quotes '{"EURUSD":1.0850,"USDJPY":150.3,...}'. Output: {"rate": <float>, "chain": ["EURUSD","USDJPY",...], "warnings": [...]}. Fetch the quote map up front via get_spot_prices for Local (one symbol per call) or get_spot_prices(symbolId:[...]) for Remote (batched).
Hedging vs netting accounts
Hedging accounts allow simultaneous long AND short positions on the same symbol (each gets its own positionId). Netting accounts collapse them: opening the opposite side automatically closes (or partially closes) the existing position to the net delta. Before placing the second leg of a hedge, read the existing position and the account's hedging mode (visible in get_balance and account-info responses); if the account is netting, "opening a hedge" is impossible — surface this to the user and propose either a stop-loss adjustment or a full close instead.
Stop-out and margin level
Brokers force-close positions when margin level = (equity / used margin) × 100% falls to or below the stop-out level (commonly 50% or 30%, broker-set). Two policies exist: fair (closes the single position consuming the most margin) and smart (closes the smallest set of positions sufficient to restore margin level above the stop-out). Before sizing additional risk, read current equity and used margin; if (equity − required_new_margin) / used_margin_after_open × 100% drops below 2× the stop-out level, warn the user before proceeding.
Swap accrual timing
Swap (overnight financing) accrues at broker server time rollover (commonly 23:59:59 server time). Many brokers triple-charge swap on Wednesday (the Wed -> Thu rollover absorbs the weekend value date for T+2 instruments like forex). When projecting swap over a holding period, multiply by 3 for any Wednesday in the window. Read swap and commission values directly from get_positions and get_deals responses (they are server-computed); do not re-derive them from rate tables.
Composable trader workflows
Seven end-to-end trader workflows are described step-by-step in references/trader-workflows.md. W0 (session bootstrap) auto-runs once at session start; W1–W6 are dispatched by user-intent triggers. When the user request matches any trigger, read references/trader-workflows.md and follow the corresponding recipe — do not improvise from scratch.
0. W0 — Session bootstrap — auto-runs at session start; identifies server family, probes live build, caches symbol precision baseline from assets/symbol_precision_table.json, resolves active account, and sets the idempotency-key prefix. No user trigger needed. 1. Position sizing by risk % — select this recipe when the user asks "how much to buy / sell", "size for N pips SL", "risk X% of my account on this trade", or any sentence combining a risk fraction with a stop-loss distance. 2. Pre-trade briefing — select this recipe when the user asks "should I trade X", "give me a snapshot of X before I enter", "what does X look like right now", or combines symbol-details + price + recent history requests into one ask. 3. Cost-of-trading comparison — select this recipe when the user asks "which is cheaper to trade, A or B", "compare spreads / commissions / swap across symbols", or ranks tradeable instruments by cost. 4. Place + visualize a trade with risk/reward annotation — select this recipe when the user asks "place the trade and show it on the chart", "draw the R:R on EURUSD", and the Local HTTP server is bound (this workflow requires drawings). 5. Multi-window historical backfill — select this recipe when the user asks for more bars than a single get_trendbars call returns (1000 cap on Local), e.g., "give me 5000 H1 candles of XAUUSD" or "show me the last year of D1". 6. Safe flatten — select this recipe when the user asks "close everything", "cancel all my orders", "flatten my book", or any request to bulk-close pending orders and open positions on one or more symbols.
Server-specific reference files
When the bound tool surface includes Local HTTP tools (ping, get_accounts_list, list_charts, listChartIndicators, listPlugins, show_notification, get_server_time), read references/local-http-server.md for the Local capability map, encoding rules, pagination caps, identifier types, active-chart targeting, drawing-object anchor requirements, and the destructive-operations checklist.
When the bound tool surface includes Remote HTTP tools (get_version, get_assets, integer symbolId-keyed tools, moneyDigits in responses), read references/remote-http-server.md for the Remote capability map, encoding rules, the 9-value period enum, server-side validations, timeInForce semantics, dealStatus enum, and the cache-discipline rules for get_symbols.
When both surfaces are bound, read both files.
Bundled scripts
Non-trivial computation lives in scripts/. Invoke a script whenever the math goes beyond a single multiplication or addition. Every script accepts CLI flags only (non-interactive), prints JSON to stdout, prints diagnostics to stderr, and documents itself via --help.
| Script | Purpose | Invocation pattern | Output shape |
|---|---|---|---|
scripts/pip_math.py | Convert between pip distance and absolute price (display values; operates on both servers' encoding once pipettes are decoded). | python scripts/pip_math.py pips-to-price --pip-size 0.0001 --digits 5 --reference-price 1.0850 --pips 30 | {"absolute_price": 1.08800, "pip_size_used": 0.0001} |
scripts/position_sizing.py | Compute order size from a risk-percent or risk-amount target, accounting for pip value and quote -> account-currency conversion. | python scripts/position_sizing.py from-risk-percent --balance 10000 --risk-pct 1 --sl-pips 30 --pip-value-per-lot 10 --conversion-rate 1.0 | {"units": 33333, "cents": 3333333, "risk_currency_amount": 100.0, "warnings": []} |
scripts/tiered_margin.py | Reproduce cTrader's dynamic-leverage margin formula across a tier curve. | python scripts/tiered_margin.py compute --volume-base-units 1000000 --quote-rate-usd 1.21345 --tiers '[{"upper":1000000,"leverage":500},{"upper":5000000,"leverage":200},{"upper":null,"leverage":100}]' | {"margin_usd": 3067.25, "per_tier_breakdown": [...]} |
scripts/conversion_rate.py | Build the shortest spot-rate chain to convert between two currencies. | python scripts/conversion_rate.py compute-chain --from-asset JPY --to-asset USD --quotes '{"USDJPY":150.3}' | {"rate": 0.006653, "chain": ["USDJPY"], "warnings": []} |
scripts/units_encoding.py | Convert between display lots and the wire encoding for each server (units / cents); convert between display money and moneyDigits integers. | python scripts/units_encoding.py lots-to-cents --lots 0.1 --lot-size 100000 | {"cents": 1000000} |
Post-order validation loop
After any mutating call (place_*_order, create_order, amend_order, amend_position, close_position, cancel_order), re-fetch the affected entity (get_positions, get_pending_orders, or get_position_details) and verify that volume, side, entry price (for fills), SL, TP, and dealStatus match the user's stated intent. If any value mismatches, identify the mismatch class:
- Encoding error -> re-run
scripts/units_encoding.pyorscripts/pip_math.pyand retry with corrected inputs. - Broker rejection (
dealStatus: REJECTED/INTERNALLY_REJECTED/ERROR) -> surface the rejection reason from the server response to the user and stop.
Do not assume the order is filled just because the call returned without an exception.
{
"__note__": "BASELINE — always verify against `get_symbol_details` at session start. Source values reflect audit on 2026-05-13.",
"schema_version": "1.0",
"as_of": "2026-05-13",
"audit_method": "live calls on rest-proxy 1.0.13 + local build observed-on 2026-05-13",
"symbols": [
{
"symbol": "EURUSD",
"pipDigits": 5,
"pipettesPerUnit": 100000,
"lotSize_baseline": 100000,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "GBPUSD",
"pipDigits": 5,
"pipettesPerUnit": 100000,
"lotSize_baseline": 100000,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "USDJPY",
"pipDigits": 3,
"pipettesPerUnit": 1000,
"lotSize_baseline": 100000,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "AUDUSD",
"pipDigits": 5,
"pipettesPerUnit": 100000,
"lotSize_baseline": 100000,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "USDCAD",
"pipDigits": 5,
"pipettesPerUnit": 100000,
"lotSize_baseline": 100000,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "NZDUSD",
"pipDigits": 5,
"pipettesPerUnit": 100000,
"lotSize_baseline": 100000,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "USDCHF",
"pipDigits": 5,
"pipettesPerUnit": 100000,
"lotSize_baseline": 100000,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "EURJPY",
"pipDigits": 3,
"pipettesPerUnit": 1000,
"lotSize_baseline": 100000,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "GBPJPY",
"pipDigits": 3,
"pipettesPerUnit": 1000,
"lotSize_baseline": 100000,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "XAUUSD",
"pipDigits": 3,
"pipettesPerUnit": 1000,
"lotSize_baseline": 100,
"source": "local",
"as_of": "2026-05-13"
},
{
"symbol": "XAGUSD",
"pipDigits": 3,
"pipettesPerUnit": 1000,
"lotSize_baseline": 5000,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "US30",
"pipDigits": 1,
"pipettesPerUnit": 10,
"lotSize_baseline": 1,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "US500",
"pipDigits": 1,
"pipettesPerUnit": 10,
"lotSize_baseline": 1,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "NAS100",
"pipDigits": 1,
"pipettesPerUnit": 10,
"lotSize_baseline": 1,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "GER40",
"pipDigits": 1,
"pipettesPerUnit": 10,
"lotSize_baseline": 1,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "UK100",
"pipDigits": 1,
"pipettesPerUnit": 10,
"lotSize_baseline": 1,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "USOIL",
"pipDigits": 2,
"pipettesPerUnit": 100,
"lotSize_baseline": 1000,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "UKOIL",
"pipDigits": 2,
"pipettesPerUnit": 100,
"lotSize_baseline": 1000,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "BTCUSD",
"pipDigits": 2,
"pipettesPerUnit": 100,
"lotSize_baseline": 1,
"source": "remote",
"as_of": "2026-05-13"
},
{
"symbol": "ETHUSD",
"pipDigits": 2,
"pipettesPerUnit": 100,
"lotSize_baseline": 1,
"source": "remote",
"as_of": "2026-05-13"
}
]
}
Copyright (c) 2026 Spotware Systems Ltd. All rights reserved.
This software is proprietary to Spotware Systems Ltd. and forms part of
the cTrader platform. Use, reproduction, modification, and distribution
are governed exclusively by the Spotware End User License Agreement,
available at:
https://www.spotware.com/eula/
By installing, accessing, or using this software, you agree to be bound
by that agreement. If you do not agree, do not use the software.
Known Quirks Ledger — cTrader MCP servers
as-of: rest-proxy 1.0.18 (Remote) + local build observed-on 2026-05-14
last_full_audit_date: 2026-05-14 (Q-R1, Q-R2, Q-R3, Q-R4, Q-R5, Q-R7, Q-R8, Q-R10 re-verified on rest-proxy 1.0.18 — all still ACTIVE; Q-R4 and Q-R7 error-format refreshed to plain-string with actionable hint; Q-R11 Verify-fixed PASSED on 1.0.18 session 1/5 — entry retained pending 4 more PASS sessions per Removal criteria)
This ledger is the single canonical source of truth on build-specific observed runtime behaviors of the ctrader-remote-mcp and ctrader-local-mcp servers as of the audit date above.
Every entry follows the canonical template (ID, Observed-on, Detect, Workaround, Verify-fixed, Removal criteria). When a server is fixed (verified by re-running the entry's Verify-fixed probe), DELETE that entry — this is the entire cleanup cost. The Workaround section names the recovery pattern (one of the P-* patterns in self-healing-playbook.md); update the playbook only if the pattern itself becomes obsolete.
Conventions
- Q-R<n> — Remote (
ctrader-remote-mcp) quirk. - Q-L<n> — Local (
ctrader-local-mcp) quirk. - Q-K<n> — Cross-cutting (both servers, or encoding-layer concern).
- Q-B<n> — Reserved for future per-broker overlay (no entries in this iteration).
The anchor for each entry is the lowercased ID (#q-r1, #q-r4-range, #q-l2, #q-k19). External cross-references (from SKILL.md, remote-http-server.md, local-http-server.md, trader-workflows.md) use the form [Q-R10](known-quirks.md#q-r10).
Each entry uses an explicit <a id="q-..."></a> HTML anchor immediately under the ### heading so cross-references remain stable even if heading text evolves.
Remote server quirks (Q-R<n>)
Q-R1 — period enum is 9 values, NOT 26
<a id="q-r1"></a>
- Observed-on: rest-proxy 1.0.13
- Detect: Live call
get_trendbars(period="M_2", symbolId=1, count=1)returns MCP-32602: Input validation error(Zod enum mismatch). The accepted set isM_1, M_5, M_15, M_30, H_1, H_4, D_1, W_1, MN_1. - Workaround: When the user requests an unsupported granularity (M_2, M_3, H_3, etc.), propose the nearest supported alternative (e.g., M_2 → M_1 or M_5; H_3 → H_1 or H_4). Never assert the legacy 26-value claim.
- Verify-fixed:
get_trendbars(period="M_2", symbolId=<valid>, count=1)returns a successful response. - Removal criteria: Verify-fixed probe passes on a current-build session; delete this entry.
Q-R2 — expirationTimestamp integer epoch milliseconds ONLY
<a id="q-r2"></a>
- Observed-on: rest-proxy 1.0.13
- Detect:
create_order(..., expirationTimestamp="2026-05-13T14:30:00Z")returns MCP-32602Zod validation error; the schema declarestype: integer. History-window endpoints (fromTimestamp/toTimestamp) DO accept both forms — this quirk isexpirationTimestamp-specific. - Workaround: Always pass integer epoch milliseconds (UTC) for
expirationTimestamponcreate_orderandamend_order. Convert any ISO string client-side before sending. - Verify-fixed:
create_order(..., expirationTimestamp="2026-05-13T14:30:00Z")succeeds and the position carries the expected expiry. - Removal criteria: Verify-fixed probe passes; delete this entry.
Q-R3 — trailingStopLoss silently dropped by create_order / amend_order
<a id="q-r3"></a>
- Observed-on: rest-proxy 1.0.13
- Detect: Place an order with
trailingStopLoss: trueoncreate_order(oramend_order); re-read viaget_positionsand observe that the flag is NOT set. The Zod schema strips unknown keys silently. - Workaround: Apply
trailingStopLoss: trueonly viaamend_position(positionId, trailingStopLoss: true, stopLoss=<anchor>)AFTER the position exists. The trail anchor is the SL level, so astopLossvalue must be present. Combine with P-AMEND-SAFE so the TP leg is preserved. - Verify-fixed:
create_order(..., trailingStopLoss=true, stopLoss=<price>)returns a position whose subsequentget_positionsread reportstrailingStopLoss: true. - Removal criteria: Verify-fixed probe passes; delete this entry.
Q-R4 — create_order with orderType: MARKET REJECTS absolute SL/TP
<a id="q-r4"></a>
- Observed-on: rest-proxy 1.0.13; behavior unchanged on rest-proxy 1.0.18 (error message reformatted).
- Detect (dual format):
create_order(orderType="MARKET", stopLoss=<price>)returns an error. Pre-1.0.18 envelope:HTTP 400 {"error":{"code":"INVALID_REQUEST","message":"SL/TP in absolute values are allowed only for order types: [LIMIT, STOP, STOP_LIMIT]",...}}. 1.0.18+ envelope (no JSON wrapper): plain string starting with"create_order: Absolute stopLoss is not supported for MARKET orders (fill price is unknown at send time). Use relativeStopLoss (offset in points from fill price) instead, ...". Both formats indicate the same underlying constraint. - Workaround (PREFERRED — single call): Use the
relativeStopLoss/relativeTakeProfitinteger-points fields oncreate_orderdirectly withMARKET(andMARKET_RANGE). Direction is implicit fromtradeSide: BUY → SL = fill − relativeStopLoss; SELL → SL = fill + relativeStopLoss; mirrored for TP. Mutually exclusive with absolutestopLoss/takeProfit. The schema declares these fields asexclusiveMinimum: 0, integer points (1 point = 1 / 10^pipDigits). Use P-REMOTE-MARKET-RELATIVE (self-healing-playbook.md§5.6). Example:create_order(orderType="MARKET", tradeSide="BUY", volume=100000, relativeStopLoss=300, relativeTakeProfit=600)on EURUSD entry 1.1709 → SL=1.1679, TP=1.1769 in one round-trip with no race window. - Workaround (FALLBACK — two-step): Apply P-REMOTE-MARKET-2STEP (
self-healing-playbook.md§5.2) ONLY when the user has stated ABSOLUTE SL/TP prices that cannot be cleanly converted to point offsets (e.g., "SL exactly at 1.16500", and the agent does NOT want to compute 1.16500 − fill price at send time): (1)create_order(orderType="MARKET", ...)WITHOUT SL/TP; (2) await fill; (3)amend_position(positionId, stopLoss=..., takeProfit=...)applying BOTH legs per P-AMEND-SAFE. Accept the small window where the position has no SL/TP between fill and amend. - Verify-fixed:
create_order(orderType="MARKET", stopLoss=<price>, takeProfit=<price>)succeeds and the response echoes both legs as absolute prices. (Note: relative-points path is NOT a Verify-fixed signal — it is an always-available alternative path, not a quirk fix.) - Removal criteria: Absolute SL/TP succeed on MARKET in the Verify-fixed probe; delete this entry. The
relativeStopLoss/relativeTakeProfitschema fields stay regardless.
Q-R4-RANGE — MARKET_RANGE SL/TP acceptance unverified (gated)
<a id="q-r4-range"></a>
- Observed-on: rest-proxy 1.0.13 (UNVERIFIED — requires live probe)
- Detect:
create_order(orderType="MARKET_RANGE", slippageInPoints=..., stopLoss=<price>)either succeeds (build accepts absolute SL/TP at creation) or returns HTTP400 INVALID_REQUEST(build behaves like Q-R4 on MARKET_RANGE). - Workaround: Run the Verify-fixed probe at session bootstrap (W0). On PASS, use P-REMOTE-MARKET-RANGE (
self-healing-playbook.md§5.3) with absolute SL/TP at creation. On FAIL, fall back to P-REMOTE-MARKET-2STEP. - Verify-fixed:
create_order(orderType="MARKET_RANGE", slippageInPoints=10, stopLoss=<price>, takeProfit=<price>)on a low-impact symbol succeeds and the response echoes both legs. - Removal criteria: Verify-fixed probe passes on the live build; delete this entry (Q-R4 may remain).
Q-R5 — IMMEDIATE_OR_CANCEL (IOC) behaves like pending LIMIT
<a id="q-r5"></a>
- Observed-on: rest-proxy 1.0.13
- Detect: Place a LIMIT with
timeInForce="IMMEDIATE_OR_CANCEL"whose volume exceeds available liquidity at the limit price; observe that the unfilled remainder PERSISTS as a working order rather than being cancelled. - Workaround: Do NOT rely on IOC for cancel-remainder semantics. Use
timeInForce="GOOD_TILL_CANCEL"with a tight cancel timer in the workflow, orMARKET_RANGEfor slippage-bounded immediate intent. If IOC is contractually required (e.g., user explicitly requests "cancel anything not filled immediately"), post-flight cancel any residual viacancel_order. - Verify-fixed: IOC with insufficient liquidity returns the partial fill AND cancels the remainder (no working order persists).
- Removal criteria: Verify-fixed probe passes; delete this entry.
Q-R7 — 720h window cap on history endpoints
<a id="q-r7"></a>
- Observed-on: rest-proxy 1.0.13; behavior unchanged on rest-proxy 1.0.18 (error message reformatted with actionable hint).
- Detect (dual format): A history request (
get_trendbars,get_order_history,get_deals) whose(toTimestamp - fromTimestamp)exceeds 720 hours returns an error. Pre-1.0.18 envelope:HTTP 400 {"error":{"code":"INVALID_REQUEST","message":"Interval between fromTimestamp and toTimestamp must not exceed PT720H",...}}. 1.0.18+ envelope: plain string"Time range exceeds upstream cap of 720h (PT720H = 30 days). Requested <X>h. Split into 720h-or-smaller windows and call this tool multiple times (the calls can run in parallel)."(includes the actual requested duration). Truncated pages within an allowed window also surface viahasMore: true. - Workaround: Apply P-REMOTE-HISTORY-CHUNK (
self-healing-playbook.md§5.5): chunk into ≤ 720h windows, loop withhasMoreadvancement, dedupe results bydealId/orderId/ bar-open timestamp. Per the 1.0.18 hint, the per-window calls CAN run in parallel. - Verify-fixed: A single request spanning > 720h returns a full result without an error and without
hasMore: true. - Removal criteria: Verify-fixed probe passes; delete this entry.
Q-R8 — Unknown-symbol asymmetry (get_spot_prices vs get_trendbars)
<a id="q-r8"></a>
- Observed-on: rest-proxy 1.0.13
- Detect:
get_spot_prices(symbolId=[999999])returns an EMPTYprices[]array silently (no error). Batch-poisoning (live on rest-proxy 1.0.14): a SINGLE unknown id in the batch returns EMPTY `prices[]` for the ENTIRE request — valid ids in the same batch are NOT partially returned, they are hidden too. By contrast,get_trendbars(symbolId=999999, ...)returns HTTP502 Bad Gatewaywith body"uProxy error: UNKNOWN_SYMBOL". - Workaround: Always validate EVERY
symbolIdin a batch against the session-cachedget_symbolsmap BEFORE callingget_spot_prices. If even one id is unknown, drop it from the batch (or surface a "symbol not in broker catalog" error) — never send a mixed batch and assume partial success. Surface a clear error message to the user when validation fails; never retry blindly. - Verify-fixed: Both endpoints return a uniform error envelope (or both succeed gracefully) for the same unknown
symbolId. - Removal criteria: Verify-fixed probe passes; delete this entry.
Q-R10 — amend_position OMIT-removes the omitted SL/TP leg (CRITICAL)
<a id="q-r10"></a>
- Observed-on: rest-proxy 1.0.13
- Detect: A position currently has BOTH
stopLossandtakeProfitset. Callamend_position(positionId, stopLoss=<new>)(TP omitted). Re-read viaget_positions(positionId)and observe thattakeProfitis now MISSING — the omitted leg was REMOVED rather than preserved. PassingstopLoss: nullis REJECTED outright (schema declares non-nullable). - Workaround: Apply P-AMEND-SAFE (
self-healing-playbook.md§5.1): on everyamend_positioncall, ALWAYS pass BOTHstopLossANDtakeProfit. To preserve a leg, re-pass its CURRENT value (read viaget_positionsfirst). Post-flight verify BOTH legs survived. - Verify-fixed: Calling
amend_position(positionId, stopLoss=<new>)with TP omitted preserves the prior TP value. - Removal criteria: Verify-fixed probe passes; delete this entry.
Q-R11 — get_deals / get_order_history have propagation lag after position close
<a id="q-r11"></a>
- Observed-on: rest-proxy 1.0.14 (first observed). Verify-fixed status on rest-proxy 1.0.18: PASSED on session 1 of 5 (just-closed deal pair
dealId 461185/461186appeared inget_dealsimmediately within the same-second window 2026-05-14). Entry retained pending 4 more consecutive PASS sessions per Removal criteria. - Detect: Close a position via
close_position(positionId, volume=<v>)and observeexecutionType: ORDER_CANCELLED(orORDER_FILLEDfor the closing deal) in the response. Then IMMEDIATELY callget_deals(fromTimestamp=<just-before-close>, toTimestamp=<now>, maxRows=50)andget_order_history(...)for the same window. On lagged builds (≤ 1.0.14), the just-closed deal/order is NOT present in either response (deals: []/orders: []/trades: []), even though the mutation response confirmed completion seconds earlier. On builds with the fix (≥ 1.0.18, pending 5-session confirmation), the deal pair appears immediately. - Workaround: PRIMARY — always prefer the
deal/order/positionobjects in the mutation response itself (e.g.,close_positionreturns the closingdealand updatedpositiondirectly) — this works regardless of build. SECONDARY (only when the mutation response is unavailable, e.g., audit/reconciliation): pollget_dealswith backoff (e.g., 5 s, 15 s, 60 s) until the expecteddealIdappears. Workflow W6 chunked-history loops should still treat the trailing tail of the window as eventually-consistent until the 5-session Verify-fixed gate clears. - Verify-fixed: Close a position; within 1 second call
get_deals(...)for the same window; the closing deal IS present in the response. Sessions PASSED so far: 1 of 5 (2026-05-14 on rest-proxy 1.0.18). - Removal criteria: Verify-fixed probe passes consistently across 5 successive sessions; delete this entry and the corresponding breadcrumbs in
references/remote-http-server.md(Pagination section) andreferences/trader-workflows.md(W6 propagation-lag note).
Local server quirks (Q-L<n>)
Q-L1 — Volume is broker-defined; lotSize may be 1
<a id="q-l1"></a>
- Observed-on: local build observed-on 2026-05-13
- Detect:
get_symbol_details("EURUSD")returnslotSize: 1,minVolume: 0.01, andplace_*_orderacceptsvolume: 0.01directly (ICMarkets Local example). The legacy "1 lot = 100 000 units" mental shortcut fails on this broker. - Workaround: ALWAYS read
get_symbol_details(symbolName)at session start and cachelotSize,minVolume,volumeStep. Encode volumes viascripts/units_encoding.py lots-to-units --lots <user-lots> --lot-size <symbol-lotSize>(--lot-sizeREQUIRED). Cross-referenceassets/symbol_precision_table.jsonfor a baseline only — verify against the live response. - Verify-fixed: This quirk is a server CONVENTION, not an anomaly; it does not get fixed. Remove this entry only if the Local server publishes a fixed lot-size standard across all brokers.
- Removal criteria: Server documentation confirms a fixed lot-size convention across brokers; delete this entry.
Q-L2 — Response-shape asymmetry on get_pending_orders (SL absolute / TP raw pips)
<a id="q-l2"></a>
- Observed-on: local build observed-on 2026-05-13
- Detect:
get_pending_orders()returns each order withstopLossas an ABSOLUTE PRICE (comparable toentryPrice) buttakeProfitas a RAW PIP DISTANCE (offset fromentryPrice). The two response fields are NOT symmetric. - Workaround: When parsing
get_pending_orders[], treatstopLossas a price andtakeProfitas a pip integer. Normalize before comparing or displaying:tp_price = entryPrice ± (takeProfit × pipSize)(sign pertradeSide). Do NOT round-trip values back to the server without re-encoding into the correct input form (stopLossPips/takeProfitPipsonamend_order). - Verify-fixed:
get_pending_orders[]returns symmetric SL/TP shapes (either both absolute or both pip distances). - Removal criteria: Verify-fixed probe passes; delete this entry.
Q-L3 — side input is case-INSENSITIVE; responses are PascalCase
<a id="q-l3"></a>
- Observed-on: local build observed-on 2026-05-13
- Detect:
place_market_order(side="BUY")andplace_market_order(side="buy")BOTH succeed identically. Response fields use PascalCase:tradeSide: "Buy"/"Sell",orderType: "Limit"/"Stop"/"StopLimit". - Workaround: Accept any user casing on input. When parsing responses, expect PascalCase and normalize to the agent's internal convention before comparison. Earlier skill versions claimed lowercase-only input — that claim was wrong.
- Verify-fixed: This is a server CONVENTION, not an anomaly; do not remove unless the input casing rule changes.
- Removal criteria: Server documentation declares a strict input casing; delete this entry.
Q-L4 — get_trendbars silently truncates above 1000 bars (truncated: true)
<a id="q-l4"></a>
- Observed-on: local build observed-on 2026-05-13
- Detect:
get_trendbars(symbolName=..., period=..., count=2000)returns at most 1000 bars and setstruncated: trueon the response envelope. - Workaround: Loop with windowed
from/toparameters; window size in minutes =1000 × timeframe-minutes. Dedupe by bar-open timestamp. Continue whiletruncated: true. (No Local equivalent of P-REMOTE-HISTORY-CHUNK is needed — local pagination is simpler.) - Verify-fixed: A request with
count: 2000returns 2000 bars andtruncated: false. - Removal criteria: Verify-fixed probe passes; delete this entry.
Q-L5 — place_*_order response is only {orderId, status}
<a id="q-l5"></a>
- Observed-on: local build observed-on 2026-05-13
- Detect: Calling
place_market_order/place_limit_order/place_stop_order/place_stop_limit_orderreturns a payload with onlyorderIdandstatus— no echoedvolume,price,stopLoss, ortakeProfit. - Workaround: After every placement, RE-READ via
get_pending_orders(pending) orget_positions(filled) to verify the placement matched intent. Treat the placement response as a receipt, not as ground truth on contents. - Verify-fixed:
place_*_orderresponses echo all submitted fields. - Removal criteria: Verify-fixed probe passes; delete this entry.
Q-L6 — Input vs response field-name asymmetry
<a id="q-l6"></a>
- Observed-on: local build observed-on 2026-05-13
- Detect: Submitted input field names differ from response field names:
limitPrice↔targetPrice,orderId↔id,expiresAt↔expiration,side↔tradeSide. - Workaround: Maintain an explicit input-to-response field-name map in the agent's normalization layer. Never assume round-trip identity.
- Verify-fixed: Input and response field names match for the same logical field.
- Removal criteria: Verify-fixed probe passes; delete this entry.
Q-L7 — get_order_history returns the trades key (not orders)
<a id="q-l7"></a>
- Observed-on: local build observed-on 2026-05-13
- Detect:
get_order_history()returns the executed trades underresponse.trades[]; the fieldresponse.ordersdoes NOT exist. - Workaround: Read
response.trades[]. Dedupe bydealId/orderIdwhen paginating. - Verify-fixed: The response key is
orders[](or bothorders[]andtrades[]are present withorders[]authoritative). - Removal criteria: Verify-fixed probe passes; delete this entry.
Q-L8 — expiresAt without Z coerced to local time
<a id="q-l8"></a>
- Observed-on: local build observed-on 2026-05-13
- Detect: Submit
place_limit_order(..., expiresAt="2026-05-13T14:30:00")(noZsuffix); observe that the resultingexpirationfield on the response or onget_pending_ordersis offset from the intended UTC moment by the client's local-time offset. - Workaround: ALWAYS append
Zto every ISO 8601 timestamp on Local input (expiresAt, drawing-objecttime1/time2/time3,get_trendbarsfrom/to). Derive reference time fromget_server_timerather than the agent's local clock. - Verify-fixed: A timestamp without
Zis rejected (preferred) or unambiguously interpreted as UTC. - Removal criteria: Verify-fixed probe passes; delete this entry.
Q-L9 — getIndicatorValues returns OLDEST-first
<a id="q-l9"></a>
- Observed-on: local build observed-on 2026-05-13
- Detect:
getIndicatorValues(...)returnsvalues[]wherevalues[0]corresponds to the EARLIEST bar in the window, not the most recent. Earlier skill versions asserted newest-first. - Workaround: Apply P-LOCAL-OLDEST-FIRST (
self-healing-playbook.md§5.4): REVERSE thevalues[]array before charting, signal generation, or downstream consumption that expects newest-first. - Verify-fixed:
values[0]corresponds to the newest bar. - Removal criteria: Verify-fixed probe passes; delete this entry.
Q-L10 — add_chart_object accepts trend_line without time anchors
<a id="q-l10"></a>
- Observed-on: local build observed-on 2026-05-13
- Detect:
add_chart_object(object_type="trend_line", price1=<float>, price2=<float>)succeeds with notime1/time2despite the JSON Schema requiring both time anchors for two-point objects. The resulting line is ill-positioned (anchored to default times) but the call does not error. - Workaround: Always pass BOTH price AND time anchors for any two-point object (
trend_line,ray,arrow_line,equidistant_channel,rectangle,ellipse, Fibonacci variants, Gann variants). Readget_chart_objectsafter placement to verify the visual position. - Verify-fixed: Submitting
trend_linewithout time anchors is rejected with a schema validation error. - Removal criteria: Verify-fixed probe passes; delete this entry.
Q-L11 — Errors as plain-text strings
<a id="q-l11"></a>
- Observed-on: local build observed-on 2026-05-13
- Detect: A failed mutation returns the error as a PLAIN-TEXT string (e.g.,
"Order error: Not enough funds to open this Position") rather than a structured{error: {code, message}}JSON envelope. - Workaround: Regex-parse the message to classify (validation / broker-rejection / resource-absent / fund-shortage / position-not-found). Apply the error-classification matrix in
self-healing-playbook.md§3 for the retry / fallback decision. - Verify-fixed: Errors are returned as structured JSON envelopes.
- Removal criteria: Verify-fixed probe passes; delete this entry.
Q-L12 — get_account_statistics may be unavailable
<a id="q-l12"></a>
- Observed-on: local build observed-on 2026-05-13
- Detect:
get_account_statistics()returns{"available": false}instead of populated statistics. The unavailability can be intermittent or persistent depending on broker configuration. - Workaround: Always check
response.availablebefore consuming statistics. Onfalse, fall back to deriving the required metrics (peak equity, max drawdown) from in-session reads (get_balancesnapshots + accumulated P&L fromget_positions/get_order_history). - Verify-fixed:
get_account_statistics()always returns populated statistics on a healthy account. - Removal criteria: Verify-fixed probe passes; delete this entry.
Q-L13 — Response enum value-name divergence
<a id="q-l13"></a>
- Observed-on: local build observed-on 2026-05-13
- Detect: Response enums use PascalCase names that DIFFER from the input enum names. Example: a price alert created with
condition: "above"is returned withconditionType: "GreaterOrEqual". The mapping is not 1:1 textual. - Workaround: Maintain explicit input-enum → response-enum mapping tables in the agent's normalization layer. Never compare response enum values directly to user-supplied input strings.
- Verify-fixed: Input and response enum value-names match (or a unified enum vocabulary is documented).
- Removal criteria: Verify-fixed probe passes; delete this entry.
Q-L15 — Active account hidden from get_accounts_list
<a id="q-l15"></a>
- Observed-on: local build observed-on 2026-05-13
- Detect:
get_accounts_list()returns one or more accounts, but the CURRENTLY active account is not in the returned list. Callingget_balance()succeeds and returnstraderId: <X>where<X>is not in the priorget_accounts_listresponse. - Workaround: Resolve the active
traderIdviaget_balance.traderId. Treatget_accounts_listas a discovery aid, not as an authoritative active-account source. Hedging mode is visible viaget_balance.accountType: "Hedged". - Verify-fixed: The active account is always present in
get_accounts_list. - Removal criteria: Verify-fixed probe passes; delete this entry.
Q-L18 — marginLevel: null is normal when no positions are open
<a id="q-l18"></a>
- Observed-on: local build observed-on 2026-05-13
- Detect:
get_balance()returnsmarginLevel: null(or omits the field) when the account has zero open positions. This is correct accounting (margin level = equity / used_margin × 100, and used_margin is 0) but consumers expecting a numeric value fail. - Workaround: Treat
marginLevel: null(or absent) as "no positions / unconstrained"; do NOT raise an error. W5 (drawdown / margin safety) must short-circuit the stop-out check whenused_margin == 0. - Verify-fixed: This is a CORRECT accounting behavior; do not remove unless the server adopts a sentinel value (e.g.,
Infinity). - Removal criteria: Server adopts an unambiguous sentinel (e.g., a documented
nullsemantic) AND the skill captures the new convention; delete this entry.
Cross-cutting quirks (Q-K<n>)
Q-K19 — Pipettes vs display foot-gun (silent market fills)
<a id="q-k19"></a>
- Observed-on: rest-proxy 1.0.13 (Remote-primary; both servers applicable when crossing encoding boundaries)
- Detect: Any 5+ digit INTEGER appearing in an order-DTO price field (
limitPrice,stopPrice,stopLoss,takeProfit) is a probable pipettes-leak — Remote market-data fields (get_spot_prices,get_trendbars) are pipettes (integer), but order/position DTO fields are DISPLAY FLOATS. Mixing produces silently wrong fills (e.g.,limitPrice=105000interpreted literally as price 105 000). - Workaround: Pre-flight gate 1.6 (
self-healing-playbook.md§1.6) flags any 5+ digit integer in a price DTO field. Always decode pipettes to display viascripts/units_encoding.py pipettes-to-price --pipettes <int> --pip-digits <int>(or equivalent inline mathdisplay = pipettes / 10^pipDigits) BEFORE submitting any mutation. Local server prices are display floats throughout, so Q-K19 does not apply to Local price fields directly; the foot-gun is on the Remote side and in any cross-server pipeline. - Verify-fixed: Remote order DTO fields accept pipettes uniformly (or display uniformly) with the encoding documented and validated; the foot-gun scope shrinks to zero.
- Removal criteria: Server documentation declares a unified encoding for market-data AND order DTO price fields; delete this entry.
Broker-overlay quirks (Q-B<n>) — reserved
Reserved for future per-broker overrides applied via assets/broker_overrides.example.json (NOT shipped this iteration; see SKILL.md extension-point note). No Q-B<n> entries are populated in this iteration.
---
If you observe a server behavior that deviates from BOTH the MCP JSON-Schema and this ledger, follow the unknown-quirk decision tree in `self-healing-playbook.md`. Provisional entries during a session are tagged `status: provisional` until corroborated by a second observation.
Local HTTP server (ctrader-local-mcp): behavior, encoding, and capabilities
as-of: local build observed-on 2026-05-14 (last live re-verification 2026-05-14; quirks first documented on 2026-05-13 remain ACTIVE; Local server was not directly probed during the 2026-05-14 Remote 1.0.18 re-audit, so this date reflects the previous full Local audit)
This document covers the BEHAVIORS, ENCODINGS, and CAPABILITY AREAS specific to the cTrader Local HTTP server (ctrader-local-mcp), which is bound to the cTrader Desktop application via a local HTTP transport. The cross-server units / pip / margin / conversion / hedging / stop-out / swap mechanics live in SKILL.md; this file adds the Local-only details that compose with them.
Surface map
The Local HTTP server exposes capability categories rooted in the cTrader Desktop application. The table below names the categories and the dominant capability area each one covers.
| Category | Capability area |
|---|---|
| Connection & diagnostics | Liveness, server-time alignment, top-level app focus and switching. |
| Accounts & balance | Account enumeration, current balance / equity / margin / margin level snapshot, lifetime account statistics. |
| Symbols & market data | Symbol universe enumeration, per-symbol static metadata + live quote, trading sessions, spot prices, historical OHLCV, forecast margin, market news. |
| Watchlists | User watchlist enumeration, creation, renaming, deletion, and symbol membership editing. |
| Trading — positions | Open-position snapshot, market entry, position SL/TP amendment, full close, partial close, bulk-close. |
| Trading — pending orders | Pending order enumeration, limit / stop / stop-limit placement, in-place amendment, single cancel, bulk cancel. |
| Trade history | Most-recent deals (executions) and closed-trade history loaded by the client. |
| Charts — lifecycle & navigation | Chart tab enumeration, open / close / focus, symbol and timeframe changes on the focused chart, viewport scroll and zoom. |
| Charts — drawing objects | Add / read / update / delete / clear-all drawing annotations on the focused chart with named object types and anchor requirements. |
| Charts — indicators | Indicator catalog and currently attached indicators on the focused chart, add / remove, parameter mutation, output-value retrieval. |
| Chart templates | Save the focused chart's styling / indicator setup as a named template; list / apply / delete templates. |
| Workspaces | Save / load / delete the full UI layout snapshot. |
| UI — layout, panels, tabs | Adjust layout mode, ASP panel and tabs, market-watch panel mode, trade-watch tab, surface notifications. |
| Price alerts | List / create / delete price-trigger alerts (above / below, bid / ask). |
| cBot plugins | Enumerate available cBots, start, stop. |
| Conventions & gotchas | Cross-cutting conventions exposed in this file: units, ISO+Z time, side casing, pip-vs-absolute split, pagination caps, identifier types, destructive-op safety. |
Capability categories above identify WHAT lives on this server; the sections below describe HOW to work with each behavior class correctly.
Volume encoding on Local
QUIRK: see Q-L1
On the Local server, every volume parameter is broker-defined: get_symbol_details(symbolName) returns the authoritative lotSize, minVolume, and volumeStep. Earlier skill versions claimed forex 1 lot = 100 000 units universally; the audit (2026-05-13) found this is BROKER-DEPENDENT. Observed example: ICMarkets Local returns lotSize: 1 for EURUSD and accepts volume: 0.01 directly. Mental shortcuts like "1 standard lot = 100 000 units" are unreliable — always read get_symbol_details before calling any volume-bearing tool. Symbol classes that historically diverged from the 100 000-units convention include metals (XAUUSD, XAGUSD), indices (cash indices such as US30, GER40), and crypto (BTCUSD, ETHUSD) — but per-broker behavior on FX itself is also broker-dependent.
Invoke scripts/units_encoding.py lots-to-units to convert from a user-stated lot size; output shape {"units": <int>}. Cross-reference SKILL.md "Units conventions across the two servers" Volume row for the Local-vs-Remote comparison.
Price encoding on Local
Prices are display floats (e.g., 1.21345). Precision is determined by the symbol's digits field returned by get_symbol_details. Pass and parse prices as-is — no scaling, no pipette conversion. Pip size (pipSize) is exposed by get_symbol_details; do not assume 0.0001 (FX majors typically 0.0001; JPY pairs 0.01; XAUUSD 0.01; indices may differ).
Invoke scripts/pip_math.py when converting pip distances to / from absolute prices on the Local server; pass --pip-size <float> --digits <int> plus the conversion-direction subcommand.
Time encoding on Local
QUIRK: see Q-L8
Every time-typed parameter (e.g., from / to on get_trendbars, time1 / time2 / time3 on drawing objects, expiresAt on pending orders) requires ISO 8601 with the explicit `Z` suffix (2026-05-12T14:30:00Z). Strings without Z are interpreted as local time by the underlying client and have produced incorrect ranges in practice. Always derive the reference time from get_server_time rather than the agent's local clock to avoid drift.
Pass timestamps through unchanged — no script needed.
Symbol identifiers on Local
Every symbol parameter is the string ticker ("EURUSD", "GBPJPY", "XAUUSD"). There is no numeric symbol id on this server. The case is exactly as the broker advertises (typically uppercase). The agent resolves symbol names from user phrasing by calling get_symbols(filter="…") when the ticker is ambiguous (e.g., "gold" → search for "XAU", "oil" → search for "WTI" or "BRENT" depending on broker).
Side enum casing on Local
QUIRK: see Q-L3
Input side is case-INSENSITIVE on the Local server as of the 2026-05-13 audit — both buy / BUY are accepted on place_market_order, place_limit_order, place_stop_order, place_stop_limit_order, and the risk_reward drawing object's side field. Response field naming uses PascalCase: tradeSide: "Buy" / "Sell", orderType: "Limit" / "Stop" / "StopLimit". Earlier skill versions claimed lowercase-only input — this was a misreading of the JSON Schema description. Casing on output uses PascalCase regardless of input.
Stop loss and take profit semantics on Local
QUIRK: see Q-L2
The Local server splits SL / TP semantics between order PLACEMENT tools (which take pip distance from entry) and the position-AMENDMENT tool (which takes absolute price). This split is Local-specific; the Remote server uses absolute price everywhere.
| Tool | SL/TP form | Field names |
|---|---|---|
place_market_order | Pip distance | stopLossPips, takeProfitPips |
place_limit_order / place_stop_order / place_stop_limit_order | Pip distance | stopLossPips, takeProfitPips |
amend_order (pending) | Pip distance | stopLossPips, takeProfitPips |
amend_position (open position) | Absolute price | stopLoss, takeProfit |
close_position, close_position_partial, close_all_positions | N/A | (no SL/TP parameter) |
When the user says "tighten SL to 1.0825" on an open position, the absolute-price form is required and amend_position is the correct tool. When the user says "30 pips SL on the new entry", the pip-distance form is required and place_*_order / amend_order are the correct tools. Mixing the two silently produces wrong levels: passing a pip integer to amend_position interprets 30 as the absolute price 30, which the broker either rejects or accepts as a destructive change.
Response-shape asymmetry (CRITICAL): As of 2026-05-13, get_pending_orders returns stopLoss as an ABSOLUTE PRICE but takeProfit as RAW PIPS — the response fields are NOT symmetric with each other. When reading get_pending_orders, treat stopLoss as a price comparable to entryPrice, and treat takeProfit as a pip distance from entryPrice. Do NOT round-trip values without normalization. This is a known server-side behavior documented in Q-L2.
Invoke scripts/pip_math.py pips-to-price to convert a pip distance into an absolute price for amend_position, or price-to-pips for the reverse direction. Input: --pip-size <float> --digits <int> --reference-price <float> --pips <int>. Output: {"absolute_price": <float>, "pip_size_used": <float>}.
Pagination caps
QUIRK: see Q-L4 and Q-L9
Every list-returning tool on the Local server has a hard cap; reading more than the cap requires multiple windowed requests with client-side de-duplication.
| Tool | Per-request cap | Pagination strategy |
|---|---|---|
get_trendbars | 1000 bars (silently truncated when more requested; flagged via truncated: true field on the response) | Loop over rolling from / to windows; window size in minutes = 1000 × timeframe-minutes; dedupe by bar open timestamp. |
get_deals | 200 deals | Issue successive calls with count=200 and use the returned timestamps to advance the window; dedupe by dealId. |
getIndicatorValues | 1000 values | Values are returned OLDEST-first as of 2026-05-13 (audit confirmed). Reverse the array before charting or downstream consumption. Iterate outputIndex 0..N-1 for multi-line indicators (e.g., MACD). |
scripts/ does not perform windowed pagination — that is a workflow-level loop described in references/trader-workflows.md workflow W6 (history).
Stop-order triggerMethod
place_stop_order and place_stop_limit_order accept triggerMethod with two values: trade (default) and opposite. trade triggers on the actual trade-side quote; opposite triggers on the opposite-side quote (ask for sells, bid for buys), which reduces premature triggers from spread spikes during news or low-liquidity sessions. This parameter does not exist on the Remote server.
When placing a stop order at a level close to the current quote during a session with widening spreads (news release, session changeover), prefer triggerMethod="opposite".
Active chart focus model
Chart-targeting tools (change_chart_symbol, change_chart_timeframe, get_chart_viewport, scroll_chart, zoom_chart, add_chart_object, get_chart_objects, update_chart_object, delete_chart_object, clear_chart_objects, listChartIndicators, addChartIndicator, removeChartIndicator, update_indicator_parameters, getIndicatorValues, save_chart_template, apply_chart_template) ALL act on the focused chart, not on a chart identified by parameter. The agent never passes chartId to these tools; instead the agent manages focus deterministically: call list_charts to enumerate, then focus_chart(chartId) to switch focus, then issue chart-targeting tools.
Forgetting to focus is the most common cause of "tool executed but I see nothing on my chart" — the operation went to a different focused chart. Before ANY chart-mutating sequence, the agent records the intended chartId and re-confirms focus with get_active_chart.
Hedging vs netting on Local
QUIRK: see Q-L15 and Q-L18
This server's positions reflect the account-level hedging vs netting mode that the broker configures. The mode is visible via get_balance (account-info shape) and via account metadata in get_accounts_list. The cross-server semantic difference between hedging and netting is described in SKILL.md "Hedging vs netting accounts"; this section names the Local-specific WAY to detect the mode (read get_balance for the active account; check get_accounts_list for multi-account context where mode may vary per account). On Local, the active account may not appear in get_accounts_list — resolve the active traderId via get_balance.traderId instead, and read get_balance.accountType to check for "Hedged". marginLevel: null on get_balance is normal when no positions are open.
State-verification before mutation
Every mutating operation on the Local server should be preceded by a read of the affected entity's current state. The pattern is: read → confirm intent → mutate → re-read to verify. The read tools and their target mutations are mapped in the table below.
| Mutation | Pre-read tool |
|---|---|
place_*_order / amend_order / cancel_order / cancel_all_pending_orders | get_pending_orders |
amend_position / close_position / close_position_partial / close_all_positions | get_positions |
add_chart_object / update_chart_object / delete_chart_object / clear_chart_objects | get_chart_objects (after focus_chart) |
addChartIndicator / removeChartIndicator / update_indicator_parameters | listChartIndicators (after focus_chart) |
delete_watchlist / remove_symbol_from_watchlist | get_watchlists |
startPlugin / stopPlugin | listPlugins |
The post-mutation re-read pattern is in SKILL.md "Post-order validation loop"; this section only specifies the pre-mutation read.
Drawing object anchor requirements
QUIRK: see Q-L10
add_chart_object requires a different combination of price anchors and time anchors per object_type. Passing only a price for a two-anchor object leaves it ill-positioned or rejected.
| Anchor pattern | Object types | Extra options |
|---|---|---|
| One-point (price1 + time1) | horizontal_line, text, static_text, markers (up_arrow, down_arrow, circle, square, diamond, star, up_triangle, down_triangle) | — (markers are visual only) |
| Time-only (time1) | vertical_line | — |
| Two-point (price1+time1, price2+time2) | trend_line, ray, arrow_line | extend (boolean) |
| Two-point geometry (price1+time1, price2+time2) | equidistant_channel, rectangle, ellipse, fibonacci_retracement, fibonacci_fan, fibonacci_arcs, fibonacci_timezones, gann_fan, gann_box, gann_square, gann_square_fixed | fill (boolean) on rectangle / ellipse only |
| Three-point (p1+t1, p2+t2, p3+t3) | triangle, fibonacci_expansion, andrews_pitchfork | fill on triangle only |
Trade visualization (risk_reward) | side ∈ {buy, sell}, p1=entry, p2=SL, p3=TP, t1=block_start, t2=block_end (t2 optional; defaults to t1) | — |
A successful add_chart_object returns an objectId — capture it for later update_chart_object / delete_chart_object calls.
Destructive operations
The Local server happily performs irreversible actions — these tools have no undo on this server, so always read current state first and confirm intent with the user.
close_all_positions— closes every open position (optionally per symbol).cancel_all_pending_orders— cancels every working order on the account.clear_chart_objects— deletes every drawing on the focused chart.delete_workspace— removes a saved layout snapshot.delete_watchlist— removes a watchlist (including its symbol membership).delete_chart_template— removes a saved styling template.delete_price_alert— removes a single alert by id.stopPlugin— stops a running cBot mid-execution.
Before invoking any of the above, read the current state with the pre-read tool from the State-verification before mutation section, and present the affected items to the user for confirmation.
Response shapes (place_*_order, amend_*)
QUIRK: see Q-L5 and Q-L6
The Local order-placement tools (place_market_order, place_limit_order, place_stop_order, place_stop_limit_order) return only {orderId, status} — no echoed volume, price, stopLoss, or takeProfit. After placement, ALWAYS re-read via get_pending_orders (pending) or get_positions (filled) to verify the placement matched intent.
Input vs response field-name asymmetries exist across endpoints — agents reading responses must remap names:
| Input field name | Response field name |
|---|---|
limitPrice | targetPrice |
orderId | id |
expiresAt | expiration |
side | tradeSide |
entryPrice | entryPrice (preserved on amend response) |
These asymmetries are server-side; the agent normalizes both directions explicitly.
Error envelopes (plain text)
QUIRK: see Q-L11
The Local server returns errors as plain-text strings, NOT structured JSON envelopes. Example: "Order error: Not enough funds to open this Position". Agents must regex-parse the message to classify the error (validation / broker-rejection / resource-absent). There is no structured error.code / error.message field.
get_order_history returns the trades key
QUIRK: see Q-L7
The get_order_history response stores executed trades under the trades key (not orders). When reading the history page, key off trades[] and dedupe by dealId / orderId. Earlier skill versions and naive integrations assume orders[]; this is incorrect on Local.
get_account_statistics may be unavailable
QUIRK: see Q-L12
get_account_statistics can return {"available": false} instead of statistics. Workflows depending on these statistics (drawdown / peak-equity tracking) must check the available flag and fall back to deriving the required metric from in-session reads.
Remote HTTP server (ctrader-remote-mcp): behavior, encoding, and capabilities
as-of: rest-proxy 1.0.18 (last live re-verification 2026-05-14; quirks first documented on rest-proxy 1.0.13 remain ACTIVE on 1.0.18; Q-R4 and Q-R7 error envelopes refreshed to plain-string with actionable hint; Q-R11 Verify-fixed PASSED session 1/5, entry retained)
This document covers the BEHAVIORS, ENCODINGS, and CAPABILITY AREAS specific to the cTrader Remote HTTP server (ctrader-remote-mcp). The cross-server units / pip / margin / conversion / hedging / stop-out / swap mechanics live in SKILL.md; this file adds the Remote-only details that compose with them.
Surface map
The Remote HTTP server exposes a smaller, headless tool surface focused on data and trading. The table below names the capability areas.
| Capability area | What lives here |
|---|---|
| Version & diagnostics | get_version (build identification). |
| Account state | get_balance (balance / equity / freeMargin / moneyDigits / depositAssetId), get_assets (asset id → currency name resolution). |
| Symbols & static metadata | get_symbols (symbolId / symbolName / enabled / baseAssetId / quoteAssetId / symbolCategoryId / description). |
| Live and historical market data | get_spot_prices (batched live quotes for an array of symbolId), get_trendbars (OHLCV by period over a timestamp window). |
| Positions, orders, deals | get_positions (open positions + pending orders), get_position_details (single position + related orders + deals), get_pending_orders, get_order_history, get_deals. |
| Trading mutations (trading profile) | create_order, amend_order, cancel_order, amend_position, close_position. |
Remote-only capabilities (trailing SL, MARKET_RANGE, batched get_spot_prices, 9-value period, explicit timeInForce, dealStatus enum, profile distinction) are described in their own sections below.
Volume encoding on Remote
Every volume field on the Remote server is an integer count of cents of the base asset (this is the wire encoding; not pennies of money). For forex 1 lot = 10 000 000 cents; this is 100× the Local server's `units` for the same lot — the conversion is critical. The Bean Validation constraint @Positive rejects non-positive values.
Invoke scripts/units_encoding.py lots-to-cents to convert from a user-stated lot size; output shape {"cents": <int>}. Cross-reference: SKILL.md "Units conventions across the two servers" Volume row for the units-vs-cents comparison.
Price encoding on Remote
QUIRK: see Q-K19
Every price field (limitPrice, stopPrice, stopLoss, takeProfit, entryPrice, executionPrice, bid, ask, high, low, sessionClose, trendbar open / high / low / close) is an integer in pipettes, where display price = price / 10^pipDigits. pipDigits is part of each symbol's static metadata (resolve via get_symbols; cache once per session).
Invoke scripts/pip_math.py when converting pip distances to / from absolute prices on the Remote server; pass --pip-size <float> --digits <int> --reference-price <float> --pips <int>. The script operates on display values; convert pipettes returned by Remote endpoints to display BEFORE passing to the script.
Money encoding on Remote
Every money field (balance, equity, freeMargin, unrealizedPnl, commission, swap) is an integer in 10^moneyDigits units. The moneyDigits value is returned on the get_balance response (typically 2, but agent reads, does not assume). To display a money figure: display = raw / 10^moneyDigits. The balanceVersion monotonic counter on get_balance increments with every balance-affecting event — useful to detect that the account state has changed between two snapshots.
Invoke scripts/units_encoding.py display-money --raw <int> --money-digits <int> to convert from wire to display; parse-money --display <float> --money-digits <int> for the reverse.
Time encoding on Remote
QUIRK: see Q-R2
Timestamp inputs and outputs come in two forms depending on the field. The Remote server's acceptance of input forms is asymmetric — see the table below.
| Field | Encoding |
|---|---|
fromTimestamp / toTimestamp (get_trendbars, get_order_history, get_deals) | Accept either epoch milliseconds (int) or ISO 8601 string ("2026-05-12T14:30:00Z"). |
expirationTimestamp (create_order, amend_order) | Integer epoch milliseconds ONLY as of rest-proxy 1.0.13. ISO 8601 strings are rejected by Zod validation. |
prices[].timestamp (get_spot_prices response) | Epoch milliseconds. |
trendbars[].timestamp (get_trendbars response) | Epoch milliseconds. |
deals[].executionTimestamp (get_deals response) | Epoch milliseconds. |
buildTime (get_version response) | ISO 8601 string (or "N/A" if missing). |
| All other response time fields | Generally epoch milliseconds. |
| Request choice rule | When sending, prefer ISO 8601 for log readability on history-window endpoints; the server normalizes to epoch internally. For `expirationTimestamp`, integer epoch ms is required. |
Passed through unchanged — no script needed.
Symbol identifiers on Remote
QUIRK: see Q-R8
Every symbol parameter is a numeric integer `symbolId` (not a string ticker). The agent resolves the user's spoken ticker to a symbolId via get_symbols (returns symbolId ↔ symbolName mapping; symbolName is the human ticker like EURUSD). The result is stable for the session — cache it.
Cross-reference SKILL.md "Units conventions across the two servers" Symbol identifier row for the Local-vs-Remote comparison.
Side enum casing on Remote
The Remote server's response always returns uppercase. Input on tradeSide is accepted in uppercase form (BUY / SELL) on create_order, amend_order, and every response that echoes a position or deal (positions[].tradeSide, orders[].tradeSide, deals[].tradeSide). Lowercase input handling is build-dependent and SHOULD NOT be relied upon — always send uppercase.
Order type enum
QUIRK: see Q-R4 and Q-R4-RANGE
orderType on create_order accepts five values: MARKET, LIMIT, STOP, MARKET_RANGE, STOP_LIMIT. The conditional-required price fields are tabulated below.
| Order type | Required price fields | Behavior |
|---|---|---|
MARKET | (none) | Fill immediately at the current bid / ask. |
LIMIT | limitPrice | Fill only at limitPrice or better. Buy-limit below ask; sell-limit above bid. |
STOP | stopPrice | Trigger as a market order when price crosses stopPrice. Buy-stop above market; sell-stop below. |
MARKET_RANGE | (none directly — see slippageInPoints) | Fill at market within an acceptable slippage band; see MARKET_RANGE and slippage section. |
STOP_LIMIT | stopPrice + limitPrice | Trigger at stopPrice, then submit a limit at limitPrice. Breakout entry with price protection. |
Critical: As of rest-proxy 1.0.13 (still active on 1.0.18), create_order with orderType: MARKET REJECTS absolute stopLoss / takeProfit. The rejection error format changed in 1.0.18 (now a plain-string hint pointing to relativeStopLoss; previously a JSON {"error":{"code":"INVALID_REQUEST",...}} envelope) — see Q-R4 for dual-format Detect. PREFERRED workaround (single call): use the schema fields relativeStopLoss / relativeTakeProfit (positive integer offset in points from fill price, direction implicit from tradeSide) on the same create_order request. Example: create_order(orderType="MARKET", tradeSide="BUY", volume=<cents>, relativeStopLoss=300, relativeTakeProfit=600) → SL = fill − 300 points, TP = fill + 600 points; mutually exclusive with absolute stopLoss/takeProfit; same pattern works with MARKET_RANGE. FALLBACK workaround (two-step), only when the user has stated ABSOLUTE prices that cannot be cleanly converted to point offsets: place MARKET WITHOUT SL/TP, then amend_position(positionId, stopLoss=..., takeProfit=...) per P-AMEND-SAFE. Prefer LIMIT / STOP / STOP_LIMIT when the user accepts a non-immediate fill; those order types DO accept absolute SL/TP at creation. MARKET_RANGE may accept absolute SL/TP at creation depending on rest-proxy build — verify with a live probe (Q-R4-RANGE) before relying on it.
timeInForce enum
QUIRK: see Q-R5
timeInForce on create_order accepts three values: GOOD_TILL_CANCEL (default-shaped), GOOD_TILL_DATE (requires expirationTimestamp), IMMEDIATE_OR_CANCEL (cancel-remainder-on-partial-fill). The Remote server exposes this explicitly; the Local server does not.
dealStatus enum
Every deal record on get_deals carries a dealStatus. The meanings drive follow-up workflow decisions.
| Status | Meaning and follow-up |
|---|---|
FILLED | Fully filled at executionPrice for filledVolume. Proceed. |
PARTIALLY_FILLED | Some of the requested volume filled; the remainder either still working (for orders that allow it) or cancelled. volume vs filledVolume differ — surface the gap. |
REJECTED | Broker rejected before any fill. Read the response context for the reason; do not retry blindly. |
INTERNALLY_REJECTED | Server-side rejection before reaching the broker (e.g., validation, profile mismatch). Treat like REJECTED for user-facing messaging. |
ERROR | Execution attempt errored out. Treat like REJECTED; the position state may or may not have moved — read get_position_details before further action. |
MISSED | The execution opportunity was missed (e.g., market gapped past a stop-limit window). No fill occurred. |
Cross-reference SKILL.md "Post-order validation loop" — that section names the action to take per status class; this table names the meanings.
Stop loss and take profit semantics on Remote
QUIRK: see Q-R10
On the Remote server, every SL / TP field is an absolute price — create_order.stopLoss, create_order.takeProfit, amend_order.stopLoss, amend_order.takeProfit, amend_position.stopLoss, amend_position.takeProfit. There is no pip-distance form anywhere on this server. This is the cleanest difference from the Local server's pip / absolute split.
Invoke scripts/pip_math.py pips-to-price --pip-size <float> --digits <int> --reference-price <float> --pips <int> to translate a user-stated pip distance into the absolute price the Remote tools expect.
Trailing stop loss
QUIRK: see Q-R3
As of rest-proxy 1.0.13, trailingStopLoss: true is silently IGNORED if passed to create_order or amend_order (Zod schema drops unknown keys). The flag is ONLY honored on amend_position(positionId, trailingStopLoss: true), applied AFTER the position has been opened. The trail anchor is the SL level, so a stopLoss value must be present (set either at order creation or via the same amend_position call). There is no trailing SL on the Local server.
MARKET_RANGE and slippage
QUIRK: see Q-R4-RANGE
MARKET_RANGE and STOP_LIMIT accept slippageInPoints (positive integer in price points) plus baseSlippagePrice (reference price for slippage calculation). The order will not fill outside the slippage band around baseSlippagePrice. The agent reaches for this combination on a market entry during volatile periods to bound execution price; on MARKET, slippage is not bounded.
MARKET_RANGE is Remote-only; the Local equivalent of bounded market entry is achieved via place_stop_limit_order with triggerMethod="opposite" (described in references/local-http-server.md).
period enum: 9 values
QUIRK: see Q-R1
The get_trendbars period parameter accepts 9 values as of rest-proxy 1.0.13:
M_1, M_5, M_15, M_30, H_1, H_4, D_1, W_1, MN_1Earlier skill versions claimed 26 values (purportedly the Swagger advertised an 18-value subset but the request accepted all 26). This is a counterfactual reading: a live get_trendbars(period="M_2", symbolId=1, count=1) returns MCP -32602: Input validation error (Zod enum mismatch). If the user requests M_2, M_3, H_3, etc., propose M_1 / M_5 / H_1 / H_4 alternatives. The Local server's 9-timeframe set is equivalent in granularity.
Server-side validations
The Remote server enforces structural constraints at the request boundary; violations return 400 Bad Request with an IllegalArgumentException message. The agent prevents these by validating before sending.
| Field / endpoint | Constraint |
|---|---|
volume (every endpoint accepting it) | @Positive — integer > 0; non-positive values rejected. |
slippageInPoints (amend_order) | @Positive — integer > 0. |
comment (create_order) | ≤ 256 characters. |
label (create_order) | ≤ 100 characters. |
get_trendbars window | fromTimestamp < toTimestamp, interval ≤ server's maxInterval, count ≤ server's limit. |
LIMIT / STOP / STOP_LIMIT orders | Corresponding limitPrice / stopPrice field MUST be present; otherwise the upstream gateway rejects. |
trailingStopLoss: true requires stopLoss (or stopPrice on stop orders) to be present — see Trailing stop loss section.
Profile distinction: data vs trading
The Remote server exposes two profiles. The data profile contains every read-only tool (get_version, get_balance, get_assets, get_symbols, get_spot_prices, get_trendbars, get_positions, get_position_details, get_pending_orders, get_order_history, get_deals) and is safe to call without confirmation. The trading profile contains everything in data PLUS the mutations (create_order, amend_order, cancel_order, amend_position, close_position); mutating tools require explicit user confirmation before invocation.
The bound tool surface tells the agent which profile is active — if no mutating tools are visible in tools/list, the connection is data-only and trade workflows cannot execute the mutation step. The agent surfaces this to the user before attempting a trade workflow.
Symbol cache discipline
The get_symbols result (the full mapping symbolId ↔ symbolName plus per-symbol enabled, baseAssetId, quoteAssetId, symbolCategoryId, description) is stable for the session. Cache it on first use; do not re-fetch per tool call. The same discipline applies to get_assets (returns assetId ↔ asset name mapping; also stable per session).
close_position requires volume
close_position on the Remote server requires a volume parameter (integer cents). To fully close, pass the position's current open volume (read from get_positions first). To partially close, pass any positive cents value ≤ current open volume, respecting the symbol's volume step. There is no "close all" without a volume argument; the Local server's close_position takes no volume and closes the full remainder by default — this asymmetry is a frequent integration pitfall.
amend_position omit-removes SL/TP (CRITICAL)
QUIRK: see Q-R10
As of rest-proxy 1.0.13, on amend_position, OMITTING the stopLoss field REMOVES the SL (and omitting takeProfit removes the TP). This is the OPPOSITE of intuition and the OPPOSITE of what earlier skill versions claimed (they said omitting leaves unchanged and null removes — both wrong). Passing stopLoss: null is REJECTED by the schema (the field is declared non-nullable).
Safe pattern: on every amend_position call, ALWAYS pass BOTH stopLoss AND takeProfit. To preserve a leg, re-pass its current value (read via get_positions first). To remove a leg, the action is to NOT call amend_position for that purpose — surface to the user that explicit removal of a leg is not directly supported and propose an alternative (e.g., close the position, or re-issue it without the leg). The Local server uses a separate convention — see references/local-http-server.md for details.
Pagination via hasMore
QUIRK: see Q-R7
QUIRK: see Q-R11 (history-propagation lag — just-closed deals do NOT appear immediately)
List-returning tools (get_pending_orders, get_order_history, get_deals) return hasMore: boolean on the response. When hasMore: true, the result is a truncated page; advance the window or invoke the next page. For get_deals, the maxRows parameter (default 50) caps page size; the agent loops with progressively advanced fromTimestamp until hasMore: false.
There is no offset / cursor token — the loop advances by timestamp boundary, so the de-duplication key is dealId / orderId.
Propagation lag on history endpoints (Q-R11): get_deals and get_order_history are eventually-consistent with respect to mutation responses. A deal that was just produced by close_position or create_order may not appear in these endpoints for N seconds to minutes. For IMMEDIATE post-mutation verification, ALWAYS use the deal / order / position objects returned in the mutation response itself; only fall back to history endpoints when the agent does not have the mutation response in hand (audit / reconciliation workflows). When relying on history endpoints near "now", poll with backoff (e.g., 5 s, 15 s, 60 s).
Rate limits
The Remote server enforces two rate-limit classes — general (most read tools and mutations) and historical (history-bearing tools). The headline rates are:
- General: 50 requests / second.
- Historical (
get_trendbars,get_order_history,get_deals): 5 requests / second.
The agent paces multi-window backfills to stay under the historical limit (e.g., insert a small interval between successive get_trendbars calls; for a 5 r/s cap, a ~250 ms spacing is sufficient).
Demo vs live distinction
The slug encodes the account environment (environment field). The Remote surface itself does not distinguish demo from live — the same tools work against either — but a live account may require an additional explicit ASK (user acknowledgement) before any mutation. The agent surfaces "this is a LIVE account" to the user before issuing the first mutation in a session against a live environment, and again for any out-of-policy size.
Self-Healing Playbook — cTrader MCP servers
as-of: rest-proxy 1.0.18 (Remote) + local build observed-on 2026-05-14 (last live re-verification 2026-05-14; named patterns end-to-end exercised on both servers; new pattern P-REMOTE-MARKET-RELATIVE added; error-classification matrix updated to cover the 1.0.18 plain-string vs JSON-envelope split)
This playbook is the executable counterpart to the "Self-healing principle" stated in SKILL.md. Apply it on every cTrader mutation; consult it when a server returns unexpected output. The playbook is intentionally compact — no schema duplication, no per-quirk Detect content (that lives in known-quirks.md). What lives here: the GATES, the MATRIX, the TREE, and the named PATTERNS.
1. Pre-flight gates
Apply every applicable gate BEFORE submitting any mutation. A gate failure that triggers STOP means the agent must surface the problem to the user, not retry.
1.1 Quote sanity (±20% with --allow-far-otm override)
- When to apply: every order-placement or amend involving a price (
limitPrice,stopPrice,stopLoss,takeProfit). - How to check: compare the price to the most recent
get_spot_prices(Remote) or symbol live quote (Local). If outside ±20% of the bid/ask band, fail the gate. - Action on fail: STOP and surface to user with the last-seen reference price. If the user explicitly invokes
--allow-far-otm(or a comparable override flag in the workflow), bypass this gate but LOG the override with the reason captured from the user.
1.2 Side-direction sanity
- When to apply: every order placement, every SL/TP amend.
- How to check: BUY-stop above current ask; BUY-limit below current bid; SELL-stop below current bid; SELL-limit above current ask.
- Action on fail: STOP and surface; suggest the symmetric tool (e.g., "this looks like a SELL-stop, did you mean STOP_SELL?").
1.3 SL/TP sidedness
- When to apply: every SL/TP value submitted alongside or atop an entry price.
- How to check: for a LONG position, require
stopLoss < entryPrice < takeProfit; for a SHORT position, requiretakeProfit < entryPrice < stopLoss. - Action on fail: STOP and surface to user with the computed gap; never auto-correct.
1.4 volumeStep compliance
- When to apply: every order placement on the Local server.
- How to check: read
get_symbol_details(symbolName).volumeStep; verifyvolume % volumeStep == 0. - Action on fail: round to the nearest valid step in the direction of the user's intent (typically toward smaller risk). Warn the user with the original and rounded values.
1.5 Schema-fields-only enforcement
- When to apply: every request, both servers.
- How to check: strip any key not declared in the MCP tool's input JSON-Schema before submitting.
- Action on fail: drop the offending key and LOG a warning naming the dropped key. Never let an unknown key reach the server (Q-R3 demonstrates the failure mode:
trailingStopLossoncreate_orderis silently dropped).
1.6 Pipettes-vs-display detection (Q-K19)
- When to apply: every order DTO submission on the Remote server.
- How to check: flag any 5+ digit INTEGER value in a price DTO field (
limitPrice,stopPrice,stopLoss,takeProfit) as a probable pipettes-leak. - Action on fail: STOP and decode pipettes to display via
scripts/units_encoding.py pipettes-to-price --pipettes <int> --pip-digits <int>BEFORE re-submitting. See Q-K19.
1.7 Required runtime fields present
- When to apply: every request.
- How to check: verify all conditionally-required fields are populated (e.g., for
create_orderwithorderType=LIMIT,limitPriceis present; forSTOP_LIMIT, bothstopPriceandlimitPriceare present; forGOOD_TILL_DATEtimeInForce,expirationTimestampis present as integer epoch ms per Q-R2). - Action on fail: STOP and surface to user with the missing-field list.
2. Post-flight verification
Every mutation requires a post-flight read of the affected entity. Treat the mutation response as a receipt, not as ground truth on contents.
2.1 Re-read after every mutation
After any place_*_order, create_order, amend_order, amend_position, close_position, close_position_partial, cancel_order, cancel_all_pending_orders: re-fetch via get_positions (open positions), get_pending_orders (working orders), get_position_details (single-position deep read), or get_order_history (closed/cancelled). Verify volume, side, entry price, SL, TP, and status match the user's stated intent.
2.2 amend_position post-flight (Q-R10): always re-read to confirm BOTH legs survived
After any Remote amend_position, ALWAYS re-read via get_positions(positionId) or get_position_details(positionId) and verify that BOTH stopLoss AND takeProfit are present and match intent. The omit-removes quirk (Q-R10) is silent at the wire level — only the post-flight read catches it. If either leg is missing, treat as a P-AMEND-SAFE violation and reissue the amend with the correct values.
2.3 place_*_order post-flight on Local (Q-L5): re-read because response carries no echo
Local placement responses carry only {orderId, status} (Q-L5). Re-read via get_pending_orders (pending) or get_positions (filled) to confirm volume, price, SL, and TP match intent.
2.4 Partial close post-flight: confirm remaining volume matches expected
After close_position_partial (Local) or close_position with partial volume (Remote): re-read the position and verify volume_remaining == volume_before - volume_closed to within the symbol's volumeStep.
3. Error-classification matrix
Classify the error envelope first; then apply the retry / fallback / surface decision.
| Error envelope | Class | Retry? |
|---|---|---|
MCP -32602: Input validation error (Zod schema mismatch) | Caller schema mismatch | NO (caller schema mismatch — fix caller) |
Remote 400 {"error":{"code":"INVALID_REQUEST", ...}} (rest-proxy ≤ 1.0.14 envelope for pre-upstream validation) | Server rejection (legacy) | NO (raise to user with reason) |
Remote plain-text starting with the offending tool name + actionable hint (rest-proxy 1.0.18+ envelope for pre-upstream validation, e.g., "create_order: Absolute stopLoss is not supported...", "Time range exceeds upstream cap of 720h...") | Server rejection (1.0.18+) | NO (raise to user, surface the embedded hint verbatim) |
Remote 502 {"error":{"code":"502 BAD_GATEWAY","message":"uProxy error: <CODE> — <description>", ...}} (unchanged on 1.0.18) | Upstream broker | NO (single retry max; suggest direct) |
Local plain-text "Order error: ..." (Q-L11) | Local fault | Regex-parse, classify, retry if idempotent |
{"available": false} (e.g., get_account_statistics — Q-L12) | Resource absent | Fallback path (see workflow guidance) |
truncated: true (Local get_trendbars — Q-L4) | Pagination | Continue with windowed loop |
hasMore: true (Remote pagination — Q-R7) | Pagination | Continue with hasMore loop |
Remote envelope-shape decision tree (rest-proxy 1.0.18 split): 1. Try-parse response as JSON. If it parses AND contains an error object with code + httpStatus keys → JSON envelope branch (legacy INVALID_REQUEST 400 OR upstream 502 BAD_GATEWAY uProxy error: 502). 2. Else, treat the body as plain string. Pre-upstream validation errors on rest-proxy 1.0.18+ begin with the offending tool name (e.g., "create_order:") or a topic label (e.g., "Time range exceeds upstream cap...") and embed an actionable workaround hint. Surface the message verbatim to the user; do NOT regex-strip it. 3. The agent should treat BOTH formats as equivalent semantically — both indicate the server REJECTED the request before reaching the broker, and the request must be corrected (not retried as-is).
4. Unknown-quirk decision tree
If observed behavior deviates from BOTH the MCP JSON-Schema AND known-quirks.md:
1. STOP the operation; do not assume a safe retry. 2. CAPTURE the exact request, the exact response, the server build identifier (get_version for Remote; observed-on date for Local), and the local timestamp. 3. SURFACE to user with the captured evidence; recommend reporting upstream to Spotware. 4. PROVISIONAL ENTRY — if the agent has a high-confidence workaround, add a provisional Q-?<n> row to known-quirks.md for the duration of the session (tag status: provisional, confidence: low). Promote to a numbered entry only after a second independent corroboration in a later session or by a different agent.
5. Named patterns (DRY recovery)
Each pattern is the canonical recovery routine for a specific quirk class. Patterns are named so workflows and reference docs can refer to them without restating the body.
5.1 P-AMEND-SAFE — read-then-amend with BOTH SL+TP always present
- When to use: every
amend_positioncall on the Remote server. - Triggering quirk: Q-R10 (omit-removes).
- How to detect: server family = Remote AND tool =
amend_position. - Steps:
1. get_positions(positionId) — capture current stopLoss + takeProfit. 2. Build the amend payload with the NEW value for the leg being changed AND the existing value for the leg being preserved. 3. Call amend_position(positionId, stopLoss=..., takeProfit=...) with BOTH legs always populated. 4. Re-read via get_positions (or get_position_details).
- Post-flight check: BOTH legs present and match intent. If either leg is missing, treat as a P-AMEND-SAFE violation and reissue the amend with the correct values.
5.2 P-REMOTE-MARKET-2STEP — place MARKET without SL/TP, then amend_position (FALLBACK)
- When to use: any
create_order(orderType="MARKET")that needs SL/TP AND the user has stated SL/TP as ABSOLUTE PRICES that cannot be cleanly converted to point offsets at send time. For the common case where the user states SL/TP as a pip-distance or accepts conversion to points, use P-REMOTE-MARKET-RELATIVE (§5.6) instead — it lands both legs atomically in one call with no race window. - Triggering quirk: Q-R4.
- How to detect: server family = Remote AND
orderType = "MARKET"AND SL/TP requested AND user requires ABSOLUTE prices (e.g., "SL exactly at 1.16500 regardless of fill"). - Steps:
1. create_order(orderType="MARKET", ...) WITHOUT stopLoss / takeProfit. 2. Await fill; capture the resulting positionId from the response or via get_positions polling. 3. amend_position(positionId, stopLoss=..., takeProfit=...) applying BOTH legs (per P-AMEND-SAFE). 4. Re-read.
- Post-flight check: position exists; BOTH legs match intent.
- Caveat: between step 1 (fill) and step 3 (amend) the position is UNPROTECTED. For high-volatility instruments or large size, prefer P-REMOTE-MARKET-RELATIVE.
5.3 P-REMOTE-MARKET-RANGE — preferred slippage-bounded entry (gated)
- When to use: slippage-bounded immediate entry when the Q-R4-RANGE Verify-fixed gate passes at session bootstrap.
- Triggering quirk: Q-R4 + Q-R4-RANGE.
- How to detect: server family = Remote AND user requires slippage bound AND Q-R4-RANGE gate = PASS.
- Gate: run the Q-R4-RANGE Verify-fixed probe in session bootstrap; on PASS use
create_order(orderType="MARKET_RANGE", slippageInPoints=..., stopLoss=..., takeProfit=...); on FAIL fall back to P-REMOTE-MARKET-2STEP. - Steps (gate PASS):
1. create_order(orderType="MARKET_RANGE", slippageInPoints=..., baseSlippagePrice=..., stopLoss=..., takeProfit=...). 2. Re-read via get_positions.
- Post-flight check: fill price inside the slippage band; BOTH legs match intent.
5.4 P-LOCAL-OLDEST-FIRST — reverse getIndicatorValues array
- When to use: every
getIndicatorValuesconsumption that expects newest-first ordering (charts, signal generation, alerts). - Triggering quirk: Q-L9.
- How to detect: server family = Local AND tool =
getIndicatorValues. - Steps:
1. Consume response.values[]. 2. REVERSE the array before charting or downstream use.
- Post-flight check:
values[0]after reversal corresponds to the most recent bar.
5.5 P-REMOTE-HISTORY-CHUNK — 720h windowed loop with hasMore + dedupe
- When to use: any Remote history fetch (
get_trendbars,get_order_history,get_deals) covering a span > 720 hours. - Triggering quirk: Q-R7.
- How to detect: server family = Remote AND tool ∈ {
get_trendbars,get_order_history,get_deals} AND(toTimestamp - fromTimestamp) > 720h. - Steps:
1. Compute window starts of size ≤ 720h covering the requested span. 2. Loop calling each window in chronological order (or in parallel — the rest-proxy 1.0.18 error hint explicitly states "the calls can run in parallel"). 3. On each response, if hasMore: true, advance the window by the last record's timestamp and continue. 4. Dedupe accumulated results by dealId / orderId / bar-open timestamp.
- Post-flight check: result count matches expectation for the requested span; no duplicates by primary key.
5.6 P-REMOTE-MARKET-RELATIVE — single-call MARKET with relativeStopLoss / relativeTakeProfit (PREFERRED)
- When to use: any
create_order(orderType="MARKET")(orMARKET_RANGE) on Remote that needs SL/TP, where the SL/TP is expressible as an integer POINT offset from fill price. This is the PREFERRED single-call replacement for P-REMOTE-MARKET-2STEP — there is no race window between fill and SL/TP application. - Triggering quirk: Q-R4 (workaround, not a fix — absolute SL/TP on MARKET remain rejected; the schema's
relativeStopLoss/relativeTakeProfitfields are the broker-supported alternative). - How to detect: server family = Remote AND
orderType ∈ {"MARKET", "MARKET_RANGE"}AND SL/TP can be expressed as integer points (1 point = 1 / 10^pipDigits; e.g., 30 pips on 5-digit EURUSD = 300 points). - Steps:
1. Convert the user-stated SL distance (in pips or in absolute price) to integer POINTS:
- From pip distance:
points = pips * (10 ^ pipDigits / 10000)(typicallypoints = pips * 10for 5-digit FX pairs; the conversion is identity for 4-digit pairs; usescripts/pip_math.pywhen uncertain). - From absolute price: NOT directly supported by this pattern (use P-REMOTE-MARKET-2STEP instead — see §5.2 caveat).
2. Submit create_order(symbolId=..., orderType="MARKET", tradeSide="BUY"|"SELL", volume=..., relativeStopLoss=<positive int points>, relativeTakeProfit=<positive int points>, label=..., comment=...). 3. Direction is implicit from tradeSide: BUY → SL = fill − relativeStopLoss; TP = fill + relativeTakeProfit. SELL → mirrored. 4. The response carries the resolved absolute stopLoss / takeProfit in position.stopLoss / position.takeProfit plus an auto-generated STOP_LOSS_TAKE_PROFIT order. Capture positionId for downstream W2 (amend) / W3 (close) workflows. 5. (Optional) Re-read via get_positions(positionId) to confirm; the values land atomically at fill so the re-read mainly serves as a sanity check, not a recovery step.
- Post-flight check: position exists with both
stopLossandtakeProfitpopulated as absolute prices that match the computed offsets from the fill price (entryPrice). No follow-upamend_positioncall is needed. - Mutual exclusion:
relativeStopLossis mutually exclusive withstopLoss;relativeTakeProfitis mutually exclusive withtakeProfit. Passing the absolute form alongside the relative form on a MARKET order will be rejected with the Q-R4 error (which itself recommendsrelativeStopLoss). - Live confirmed: rest-proxy 1.0.18 audit on 2026-05-14 executed
create_order(MARKET, BUY, vol=100000, relativeStopLoss=300, relativeTakeProfit=600)on EURUSD — position 109335 opened at 1.1709 with SL=1.1679, TP=1.1769 in one round-trip.