diff --git a/README.md b/README.md index 2f0dd5f..049d180 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,9 @@ After installation, run your coding agent from the directory where you installed The skills themselves do not require any API keys. For cTrader account setup, MCP server installation, and platform documentation, see the [cTrader Help Centre](). -## Available Skills (1) +## Available Skills (2) +- **ctrader-cli** -- Use whenever driving the cTrader CLI executable (`ctrader-cli.exe` on Windows, `ctrader-cli` on macOS and Linux) from a shell. Covers the batch and shell-routed invocation pipelines and how the choice changes payload casing and accepted flags, environment-variable credentials that keep secrets out of the command line, the exit-code and stream contract for reliable failure detection, the read-only command surface with its real field names, a guarded sequence for state-changing commands, and the cBot create-build-metadata-backtest loop. Includes a session preflight the agent runs before its first authenticated call, with the exact setup instructions to give a user whose CLI is not yet configured. - **ctrader-mcp-servers** -- Always use when working with any cTrader MCP server. Covers Local HTTP server semantics, Remote HTTP server semantics, units and encoding conventions, end-to-end trader workflows, the build-stamped runtime-behavior reference, and named operational patterns the agent applies on every call. Bundles five executable helper scripts (pip math, position sizing, conversion rate, tiered margin, units encoding) that the agent invokes for precision-critical calculations. ## License diff --git a/skills/ctrader-cli/LICENSE.txt b/skills/ctrader-cli/LICENSE.txt new file mode 100644 index 0000000..8d18c4b --- /dev/null +++ b/skills/ctrader-cli/LICENSE.txt @@ -0,0 +1,11 @@ +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. diff --git a/skills/ctrader-cli/SKILL.md b/skills/ctrader-cli/SKILL.md new file mode 100644 index 0000000..bdd8faf --- /dev/null +++ b/skills/ctrader-cli/SKILL.md @@ -0,0 +1,154 @@ +--- +name: ctrader-cli +description: Use this skill whenever you drive the cTrader CLI executable (ctrader-cli.exe) from a shell - querying accounts, symbols, prices, candles, orders, positions, deals or exposure; placing, modifying or cancelling orders; managing price alerts; scaffolding, building, backtesting or running cBots and indicators. +allowed-tools: "Read, Grep, Glob, Bash(ctrader-cli.exe *), Bash(timeout 30 ctrader-cli.exe *), Bash(timeout 60 ctrader-cli.exe *), Bash(timeout 240 ctrader-cli.exe *), Bash(ctrader-cli *), Bash(timeout 30 ctrader-cli *), Bash(timeout 60 ctrader-cli *), Bash(timeout 240 ctrader-cli *), Bash(command -v *), Bash(printenv *)" +compatibility: "Requires the cTrader CLI on any OS: Windows installs it with `winget install Spotware.cTrader.CLI` (executable ctrader-cli.exe); macOS and Linux install it with Homebrew from the Spotware tap (executable ctrader-cli). Command shapes are written for bash (Git Bash on Windows or any POSIX shell) using the Windows executable name; on macOS and Linux drop the .exe suffix." +license: Proprietary. LICENSE.txt has complete terms. +metadata: + author: "Spotware Systems Ltd" + cli_build_observed: "5.9.0.38" + last_full_audit_date: "2026-08-03" +--- + +## Overview + +The cTrader CLI is a headless client for the cTrader platform: accounts and symbols, market data, orders and positions, history, price alerts, and the full cBot lifecycle, all without the desktop UI. + +It offers two invocation pipelines, and choosing the right one is the single most valuable thing to internalize. **Batch** verbs return compact machine-readable payloads for scripting. **Shell-routed** verbs run through the interactive command shell, which reaches a much larger command surface and returns a richer payload. The same verb can be served by either pipeline, and the pipeline you land in determines the JSON casing, the payload wrapper, and which flags are accepted. Everything below exists to make that choice deliberate rather than accidental. + +The CLI runs natively on every platform. On Windows it installs with `winget install Spotware.cTrader.CLI` as `ctrader-cli.exe`; on macOS and Linux it installs from the Spotware Homebrew tap (`brew tap spotware/tap https://github.com/spotware/homebrew-tap`, then `brew install spotware/tap/ctrader-cli`) as `ctrader-cli` — the same CLI with identical verbs, flags, and environment variables. Command shapes throughout this skill use the Windows executable name; on macOS or Linux drop the `.exe` suffix and everything else carries over unchanged (`references/setup.md` covers the per-OS credential setup). + +Behavior described here matches build `5.9.0.38`; re-confirm details that matter on a newer build. + +## Invocation discipline + +Use this template for every call, adjusting only the timeout: + +```bash +timeout 30 ctrader-cli.exe [flags] -e out.txt 2>err.txt; echo "EXIT:$?" +``` + +Each element earns its place: + +- `timeout` bounds every call. Use 30 seconds for data commands and 240 for `build`, `backtest`, and `run`. +- `` and `2>` captures, with **unique file names per invocation**, keep the payload away from the command echo. Concurrent sessions on a shared machine can otherwise clobber fixed names. +- Read `$?` directly. A pipe reports the exit status of the last stage, so piping into `head` or `grep` discards the CLI's own code. + +Budget roughly 1.5 to 2 seconds of process start-up per call. When you need several read-only results, fold them into one process through piped stdin, which amortizes that cost: + +```bash +printf 'accounts\nsymbols\nq\n' | timeout 60 ctrader-cli.exe -e --account= +``` + +Two commands run in 4.1 to 4.4 seconds this way versus 5.1 to 5.8 seconds as separate calls. Blank lines and lines beginning with `#` are ignored, so a piped script may carry comments. Note that a piped session is a shell session: its output arrives in **shell-routed shape** (camelCase, wrapped objects), not batch shape. + +On Windows, bash consumes backslashes in unquoted arguments, so always double-quote Windows paths: `--report-json="C:\reports\run.json"`. + +## Routing: batch verbs and shell-routed verbs + +Six verbs have a genuine batch code path: `periods`, `accounts`, `symbols`, `metadata`, `run`, `backtest` — the six that `--help` itself names for BATCH mode. `create` and `build` appear in `--help`'s BATCH MODE reference section, but they execute through the command shell in every invocation shape — banner first, then a camelCase JSON payload, and without `-q` a fall-through into the interactive menu — so call them with `-q`. Everything else is reached only through the command shell. + +`-q` / `--quick` is a **routing flag**, not a quiet flag. Adding it to one of the six batch verbs moves the whole invocation into the shell pipeline, which changes the payload casing, wraps it under a command-name key, prepends a banner and a timestamp header, and can change which flags are accepted. + +| Verb class | Call it | Payload | +| ------------ | --------- | --------- | +| Batch data (`accounts`, `symbols`, `metadata`) | **without** `-q` | PascalCase JSON, no banner | +| Batch streaming (`run`, `backtest`) | **without** `-q` | `backtest`: plain-text progress log, then a final compact PascalCase JSON summary; `run`: streams the algo's output until stopped | +| Scaffolding (`create`, `build`) | **with** `-q` | camelCase JSON after the banner and timestamp header | +| Shell-routed (`account`, `account-stats`, `symbol`, `sessions`, `price`, `prices`, `candles`, `orders`, `positions`, `order`, `position`, `exposure`, `orders-history`, `deals`, `alerts`, `alert`, `indicators`, `indicator`) | **with** `-q` | camelCase wrapped under the command name, after a banner | +| `periods` | bare, **no flags at all** | plain text, no credentials needed | + +Concrete consequences worth knowing before your first call: + +- `accounts -e` does not accept `--account`; `symbols -e` requires it. The two batch verbs differ deliberately. +- Batch `symbols` returns `Id`, `Name`, `Description` per entry. The same verb with `-q` returns `name`, `description`, `category`, `assetClass` — it swaps the numeric `Id` for classification fields rather than extending the batch schema, so use batch `symbols` whenever you need symbol ids. +- `periods` takes no flags whatsoever; passing `-e` to it returns `Error: Parameter e is not allowed`. +- Shell-routed verbs take their arguments as explicit flags at launch time. `symbol --symbol=EURUSD` works; a bare `EURUSD` positional does not. +- `-q` is recommended rather than required for shell-routed reads: it skips the command menu that otherwise prints after the payload, keeping stdout minimal for parsing. Without it the call still terminates cleanly under redirected stdin, printing the menu once and then `Bye.` at exit 0. +- An unrecognized verb routes to the shell menu and exits 0 under closed stdin, so never treat "exit 0" alone as proof the verb you intended actually ran. + +`ctrader-cli.exe --commands` prints the full shell command reference, needs no credentials and no network, and is the zero-cost way to confirm a verb's exact argument forms. + +## Credentials + +Two credential shapes work non-interactively. Prefer the first, which keeps secrets out of the process arguments entirely. + +```bash +# Environment-variable credentials: CTID and PWD-FILE are set in the environment. +timeout 30 ctrader-cli.exe accounts -e ...`. Redact it when quoting captured output into reports or anything shared. + +## Reading the output + +Locate the payload structurally, never by a fixed line count or byte offset. Scan for the timestamp header line or the first balanced `{` or `[`; transient connection-retry lines can precede a payload that still arrives successfully. + +Batch payloads begin at byte zero: no banner, no header, flat PascalCase JSON. Shell-routed payloads carry a banner of several lines whose wording varies, then a header line of the form `[yyyy-MM-dd HH:mm:ss ]` echoing the command, then camelCase JSON wrapped under the command name such as `{"orders": [...]}`. That header is local wall-clock time with the machine's own zone offset, which is distinct from timestamps inside the JSON body: those are UTC with a `Z` suffix. + +Casing therefore follows the pipeline, not the verb. `accounts` returns `Id` and `Balance`; `orders` returns `id` and `volumeLots`. `backtest` is worth singling out: its compact stdout summary is PascalCase (`Equity`, `NetProfit`) while the file it writes for `--report-json` is camelCase (`main`, `equity`, `tradeStatistics`). `build` takes no report flags; its stdout payload is camelCase (`projectPath`, `success`, `errors`). + +Stderr carries the CLI's own echo of the command line on batch-routed calls, on success as well as failure, so its mere presence never signals an error. Shell-routed calls typically leave stderr empty. Because of that split, treat stderr as a hint about which pipeline you reached, and never as a pass/fail signal. + +Plain-text rather than JSON output comes from `periods`, `--help`, `--commands`, `--version`, confirmation and refusal messages, and not-found messages such as `Order #N not found.`. The timestamp-header rule applies to shell-routed JSON commands only. + +Line endings are CRLF on Windows. Numbers are plain JSON numbers, already rounded by the CLI's own formatting, with no locale-dependent separators; the one exception is a zero-trade backtest summary, which renders `AverageTrade` and `ProfitFactor` as a bare `-` token that strict JSON parsers reject. + +## Exit codes and failure detection + +| Code | Meaning | +| ------ | --------- | +| 0 | Success | +| 1 | Invalid usage, unrecognized flag, missing required parameter, or a validation error | +| 81 | Invalid cTrader ID or password | +| 82 | Account cannot be found | +| 124 | The external `timeout` ended the process | + +Message text is the primary detection signal and the exit code is the branch key. Nonzero codes outside this table exist for further error conditions but are not publicly documented, so read the message rather than assuming a numbered meaning. A wrapper shell can report a different code than a direct invocation, so prefer the message when the two disagree. + +Never infer failure from the exit code alone. `build` exits 0 for a successful compile **and** for a failed one: the JSON body's `success` boolean is the authoritative signal, with diagnostics in `errors: [{file, line, column, code, text}]`. `backtest` continues running after printing its summary, so the completion signal is the final JSON summary or the `--report-json` file appearing; a 124 with those artifacts present is a completed run, not a failure. + +A non-numeric value for a numeric flag exits 1, but the message shape depends on the flag and the pipeline: `candles --count=abc` returns a clean `Error: Option --count has invalid number: abc` line on stdout, `--account=abc` on batch-routed `symbols` surfaces a `System.FormatException` trace, and `--account=abc` on shell-routed verbs such as `orders` prints `Invalid account number: 'abc'. Expected a numeric login id.` on stderr with no `Error:` prefix. No single stream or prefix covers this class, so validate numeric flag values before invoking. + +## Mutating commands + +The state-changing verbs are `order place-market`, `order place-limit`, `order place-stop`, `order place-stop-limit`, `order modify`, `order cancel`, `position close`, `position close-partial`, `position modify`, `alert create`, `alert delete`, and `stop`. + +**Authorization gate.** On a mutating verb, `-q` *is* the confirmation: it answers the confirmation prompt automatically and the command executes. Because `-q` is also the routine flag for shell-routed reads, it is easy to carry over by habit. Add `-q` to a mutating command only after the user has explicitly authorized that specific action, and only after you have confirmed the target account is a demo account by checking that `accounts` reports `"Live": false`. Never run a mutating command to discover how it behaves. + +At a flag-style launch line, `-q` is the confirmation form that applies. `--yes` and `-y` return `Refused: confirmation required` and exit 1. A trailing bare `yes` is shell-prompt syntax; mixing it into a flag-style line ends the process with a `ConsoleInvalidUsageException` usage trace at exit 1. The `all` keyword likewise belongs to the shell prompt; `--help` documents a separate `--all` launch flag — verify it before relying on it. + +**Volume is expressed in units by default.** On EURUSD, `--volume=1000` with no `--volume-type`, `--volume=1000 --volume-type=units`, and `--volume=0.01 --volume-type=lots` all resolve to the identical stored order of `volume: 1000, volumeLots: 0.01`. Read the instrument's own limits first with `symbol --symbol=`, which returns `lotSize`, `minVolume`, `maxVolume`, `volumeStep`, `digits`, and `pipSize`. + +**Stop loss and take profit are three-state on modify.** A value replaces, `0` removes, and omitting the flag preserves the current value. Supply prices as absolute values. A price on the wrong side of the reference is rejected before submission with a two-line `Warning:` then `Error:` message at exit 1, leaving server state untouched. + +Use the exact flag names `--order=`, `--position=`, and `--alert=`. Response keys differ per command: `order place-limit` returns `orderId` with `status: "placed"` (pending placements share this shape; expect `positionId` with `status: "opened"` from a market order that fills immediately), `alert create` returns `id` with `status: "created"`, and `alert delete` returns `alertId` with `status: "deleted"`. Read the key that the command you called returns. + +After every mutation, re-read the affected entity with `orders`, `positions`, or `orders-history` and confirm the applied state before reporting success. + +## Session preflight + +Before the first authenticated call, run the four-step preflight in `references/setup.md`: executable reachability, presence-and-length-only checks of `CTID` and `PWD-FILE`, password-file sanity, and a single go/no-go probe with `accounts -e`. The three outcomes are exit 0 with a JSON account array (ready), exit 81 (credentials rejected), and exit 1 with a missing-parameter message (a fast, clear failure, never a hang). That reference also carries the exact wording to give a user whose configuration needs fixing. + +## Reference files + +- `references/setup.md` - the preflight sequence and the user-facing setup instructions for each failure mode. +- `references/routing-and-output.md` - the full routing matrix, payload anatomy, and parsing recipes. +- `references/commands.md` - per-verb flags and payload shapes for the read-only surface. +- `references/trading-write.md` - the guarded mutation sequence, volume and protection semantics, and response shapes. +- `references/algo-lifecycle.md` - create, build, metadata, backtest, and run for cBots and indicators. +- `references/errors-and-exit-codes.md` - the failure table, message shapes, and recovery actions. diff --git a/skills/ctrader-cli/references/algo-lifecycle.md b/skills/ctrader-cli/references/algo-lifecycle.md new file mode 100644 index 0000000..9edd324 --- /dev/null +++ b/skills/ctrader-cli/references/algo-lifecycle.md @@ -0,0 +1,137 @@ +# Algo lifecycle reference + +This reference covers the cBot and indicator developer loop: `create`, `build`, `metadata`, `backtest`, `run`, and the cleanup call `stop`. +The build JSON contract, the metadata-driven override flags, and the backtest completion signal each work differently from the read-only verbs, and a habit carried over from those verbs will misfire here. + +## The authenticated loop + +`create`, `build`, `metadata`, `run`, and `backtest` are the algo-lifecycle verbs — `metadata`, `run`, and `backtest` with a genuine batch code path, `create` and `build` shell-routed in every invocation shape (call them with `-q`) — and `-e` with `CTID`/`PWD-FILE` set in the environment is sufficient for all of them: no `--ctid`/`--pwd-file` flags are needed on the command line once the environment variables are present. `metadata` is callable with no account at all, exactly like `periods`; a nonexistent algo path still routes through the parser rather than an auth prompt, returning exit 1 with `Unable to determine destination for argument value: ` and empty stderr, identically whether `-e` is present or not. `create` and `build` do authenticate, and the full account handshake happens before either scaffolds or compiles files on disk. + +Run this loop in order for a new algo: `create` to scaffold, edit the generated source, `build` to compile, `metadata` on the resulting `.algo` to discover its parameters and access rights, then `backtest` or `run` with the confirmed override flags. + +## create: scaffolding a project + +Use `--name=` explicitly. A positional name after `create cbot ` does not reach the parser reliably: an invocation with a bare positional and no `--name` returns exit 1 with `Error: 'name' is required. Provide it as a positional argument inside the shell, or pass --name= when launching the command.`, followed by the full interactive command menu, because any missing or misparsed required argument on these verbs drops the process into that menu rather than producing a clean batch error. Read the menu's presence in your captured output as the parser-fallback signal, not as evidence the wrong thing happened silently. + +The working shape is: + +```bash +timeout 30 ctrader-cli.exe create cbot --name= -e -q ", + "language": "csharp", + "projectPath": "", + "projectFile": "", + "mainFile": "", + "expectedAlgoFile": "", + "status": "created" +} +``` + +`expectedAlgoFile` is exactly where the compiled `.algo` lands after a later `build`, so capture it and reuse it rather than reconstructing the path yourself. The name must be a valid identifier in the target language (`--help` documents this constraint for `--name`; for a `csharp` project that means a valid C# identifier). The JSON shape above is the `csharp cbot` case; the `create indicator` and `create cbot python` variants follow the same shape by construction — re-confirm their `kind` and `language` fields on first use. + +## build: the JSON contract + +Point `build` at the project produced by `create`: + +```bash +timeout 240 ctrader-cli.exe build --project-path="" -e -q ", + "success": true, + "errorCode": null, + "errors": [], + "warnings": [] +} +``` + +A failed compile returns the same shape with `success: false` and populated diagnostic entries, each carrying `file`, `line`, `column`, `code`, and `text`: + +```json +{ + "projectPath": "", + "success": false, + "errorCode": null, + "errors": [ + { "file": "", "line": 12, "column": 9, "code": "CS1002", "text": "; expected" } + ], + "warnings": [] +} +``` + +Diagnostic codes follow Roslyn's own numbering (`CS1002`, `CS0103`, and so on), so a `code` value is directly searchable against standard C# compiler documentation. Read `success` first, then walk `errors` for anything that needs fixing before your next `build` attempt; a nonempty `warnings` array alongside `success: true` is not a failure. + +## metadata: discovering the real override flags + +Run `metadata` on the built `.algo` file before attempting a `run` or `backtest` override: + +```bash +timeout 30 ctrader-cli.exe metadata "" ", + "Type": "cBot", + "AccessRights": "None", + "BuildTime": "", + "Parameters": [ + { "PropertyName": "Message", "FriendlyName": "Message", "Type": "String", "DefaultValue": "Hello world!" } + ] +} +``` + +`Parameters[].PropertyName` is not a documentation placeholder; it is the literal identifier you append after `--` to override that parameter on a later `run` or `backtest` call. A parameter named `Message` becomes `--Message=` on the command line; the backtest parameter table's source tags show whether an override actually took (see Parameter precedence below). Treat `metadata` as the mandatory discovery step rather than guessing a generic `--CustomParameter1=`-style flag from the general help text; the real flag name is always the exact `PropertyName` string, case preserved. `AccessRights` predicts whether `run` will need `--full-access`: the CLI's help describes the flag as disabling the access-rights sandbox on `run`, and the official documentation pairs the error `Additional AccessRights are required.` with `--full-access` as the remedy — so expect a `"FullAccess"` value to call for the flag and `"None"` not to, and confirm against a `FullAccess` algo on first use. + +## Parameter precedence + +When several sources set the same parameter, the expected order of precedence, lowest to highest, is: the algo's own compiled default, an environment-variable override, a supplied `.cbotset` file, and finally a command-line `--PropertyName=value` flag. The flag-over-default step is certain; verify the middle rankings with the parameter table's source tags before relying on them. If the ordering holds, you can keep a shared `.cbotset` baseline for a symbol/period combination and layer a per-invocation `--PropertyName=value` override on top without regenerating the whole settings file each time. + +Whichever invocation shape you use, check the pre-flight parameter table before trusting a run: an invocation that is not launched with `-q` (or that is otherwise shell-routed) prints a "Collected parameters" table tagging each value's source as `cmd arg` or `default value`. Read that source tag before the run proceeds; it is the direct way to catch a parameter that silently fell back to its default because a flag name did not match the `PropertyName` you expected. + +## run: launching an instance + +`run` batch mode takes one `--PropertyName=value` flag per parameter override, discovered from `metadata` as above; the comma-separated `--robot-params=k=v,...` syntax belongs to the interactive shell prompt only and is not recognized in batch mode. Pass `--full-access` when `metadata`'s `AccessRights` field reports `"FullAccess"` (see the metadata section above). + +Batch `run` blocks in the foreground, streaming output to stdout until the instance exits; pass `--exit-on-stop` when you want the process to end on its own once the algo stops itself, and otherwise plan to terminate it externally (the external `timeout` wrapper, or a signal) rather than expecting it to return control to you. Confirm the exact flag spelling and blocking shape with a live invocation before depending on timing-sensitive automation around it. + +## backtest: date format, positional form, and completion signal + +`--start` and `--end` take `dd/MM/yyyy` (or `dd/MM/yyyy HH:mm`) exclusively. The authoritative usage line in `--help` shows only `--start=
` and `--end=
` for `backtest`, even though the same help text's generic OPTIONS REFERENCE section lists `--from`/`--to` as applicable to `backtest` alongside `candles`, `deals`, and `orders-history`. That generic listing does not hold for `backtest` in practice: passing `--from`/`--to` to `backtest` reroutes the call into the interactive shell and ultimately fails with `Error: 'algo-file' is required...`, the same shell-reroute behavior seen on other missing required arguments. Plain ISO `yyyy-MM-dd` is rejected outright with `Error: Value for parameter start can't be parsed`. Use `--start=`/`--end=` in `dd/MM/yyyy` form and nothing else. + +Pass the algo file as a **positional** argument, not as `--algo-file=`. The positional form is the one to prefer: passing the path through the named `--algo-file=` flag changes validation order so that a malformed `--start` value is accepted rather than rejected, and the run silently falls back to a default trailing window of roughly seven days instead of raising the parse error the positional form produces. The positional form surfaces a bad date immediately; the named-flag form can substitute an unintended window without a validation failure to flag it. Always check the "Collected parameters" table's `Start`/`End` rows and their `cmd arg`/`default value` source tags before trusting a launched backtest, regardless of which form you used. + +The working shape is: + +```bash +timeout 240 ctrader-cli.exe backtest "" --start=01/06/2026 --end=15/06/2026 --data-mode=m1 --balance=1000 --account= --symbol=EURUSD --period=h1 --report-json="" -e --account= -e -q \\Backtesting\`; on other platforms the cAlgo root differs, and the absolute paths that `create` reports (`projectPath`, `expectedAlgoFile`) reveal where that tree lives on the machine at hand — regardless of whether `--report` or `--report-json` was passed, containing `events.json`, `log.txt`, `parameters.cbotset`, and `report.html`. When cleaning up after a probe or a scratch run, check this location in addition to the explicit `--report-json` path and the project tree that `create` reported at `projectPath`/`expectedAlgoFile`. diff --git a/skills/ctrader-cli/references/commands.md b/skills/ctrader-cli/references/commands.md new file mode 100644 index 0000000..c02dc97 --- /dev/null +++ b/skills/ctrader-cli/references/commands.md @@ -0,0 +1,123 @@ +# Command reference: the read-only surface + +Per-verb detail for every command that only reads data. Each entry gives the pipeline it uses, the flags that make it work, the exact field names and casing of the payload, and the shape you get back when there is nothing to return. Field names are quoted verbatim from live payloads. + +## accounts + +Batch verb. Call it as `accounts -e` (no `--account`; the batch form rejects that flag with `Error: Parameter account is not allowed`). Payload is a flat PascalCase array with no banner and no header, stdout starting at byte zero: `[{"Id":47246474,"Number":5816091,"Broker":"Spotware","Live":false,"DepositCurrency":"EUR","Leverage":100,"Balance":1000.0}]`. `Live: false` is the field to check before authorizing any mutation on the account. Adding `-q` reroutes the same verb into the shell pipeline: the payload becomes `{"accounts":[{"traderLogin":...,"brokerName":...,"depositCurrency":...,"environment":"demo","isCurrent":true,"balance":...,"accountName":null,"leverage":...,"accountType":"Hedged","accessRights":"FullAccess","accountStatus":"Active","isSwapFree":false,"isLimitedRisk":false,"traderId":...}]}` after a banner and header, with several fields (`environment`, `accountType`, `accessRights`, `accountStatus`, `isSwapFree`, `isLimitedRisk`, `traderId`) that the batch shape does not carry at all. An unrecognized `--broker=` value returns exit 0 with an empty array `[]`, not an error; treat the empty array as "no accounts match that filter." + +## symbols + +Batch verb. As with `accounts`, the batch and shell-routed forms genuinely differ in schema, not just casing. Call it `symbols --account= -e`: this batch form requires `--account` (the inverse of `accounts`, which forbids it). The batch payload is PascalCase with only `Id`, `Name`, `Description` per entry, no banner: `[{"Id":1,"Name":"EURUSD","Description":"Euro vs US Dollar"}, ...]`. Expect hundreds of entries from a typical broker. Add `-q` and the same verb becomes shell-routed: the payload wraps as `{"symbols":[{"name":...,"description":...,"category":"Default Category","assetClass":"Forex"}, ...]}` — the entries gain `category` and `assetClass` but carry no id field in any casing, so the numeric `Id` exists only in the batch shape. Omitting `--account` on the batch form returns exit 1 with `Error: Option account does not exist` followed by the full usage block on stdout. Because this payload is large, filter by name client-side or use `symbol --symbol=` for one instrument's detail rather than loading the full array into context. + +## periods + +Bare invocation only, no flags whatsoever, no credentials: `ctrader-cli.exe periods`. Passing `-e` to it returns exit 1 with `Error: Parameter e is not allowed`. The payload is plain text, not JSON: a space-separated token list such as `t1 t2 t3 ... HMonth1`, mixing lowercase (`m1`, `h1`, `t1`) and capitalized (`D1`, `W1`, `Month1`) family names. Parse it as whitespace-separated tokens. Period tokens accepted by other commands (`candles`) are case-insensitive even though this canonical list shows one casing per family; a call with `--period=d1` succeeds and echoes back `"timeframe": "d1"` verbatim rather than normalizing to the list's casing. + +## account + +Always shell-routed; there is no batch code path for this verb regardless of flags. Call it `account --account= -e`. A bare positional number at launch (`account 5816091 -e`) exits 0 but is silently discarded — the payload is the default account's regardless of the number passed — so always use the `--account` flag, which is applied and validated. Payload after the banner and `[timestamp] account ` header is a camelCase object: `isCurrent`, `balance`, `equity`, `margin`, `freeMargin`, `marginLevel`, `netProfit`, `grossProfit`, `leverage`, `depositAsset`, `accountName`, `accountType`, `traderId`, `brokerName`, `connectionState`, `isSwapFree`. + +## account-stats + +Always shell-routed, same wrapper as `account`. Call it `account-stats --account= -e`. On an account with no trading history, the payload collapses to a single boolean: `{"available": false}`. No public source documents the populated payload, so discover its shape from a live call against an account with history rather than assuming fields. + +## symbol + +Always shell-routed. A bare positional symbol name does not work at launch (`symbol EURUSD -e` returns exit 1: `Error: 'symbol' is required. Provide it as a positional argument inside the shell, or pass --symbol= when launching the command.`). The working form is `symbol --symbol= --account= -e`. Success payload: `{"name","description","category","assetClass","digits","pipSize","lotSize","minVolume","maxVolume","volumeStep","bid","ask"}` — the complete field set needed for order sizing (for EURUSD: `digits=5`, `pipSize=0.0001`, `lotSize=100000`, `minVolume=1000`, `maxVolume=10000000`, `volumeStep=1000`). There is no commission field and no swap field in this output. Commission is a `backtest` input (`--commission`, alongside `--spread`); no swap flag exists on any command, and the only swap-related field anywhere in the CLI surface is the `isSwapFree` boolean in the `account`/`accounts` payloads. A resolvable-format but nonexistent name returns a distinct exit 1: `Error: Symbol not found: NOTAREALSYMBOL` — different from the missing-flag error above, so branch on message text, not just exit code. + +## sessions + +Always shell-routed. Call it `sessions --symbol= --account= -e`. Payload: `{"symbolName","timeZone","marketIsAlwaysOpen","sessions":[{"start","end","startSecond","endSecond"}, ...]}`. For EURUSD, five session windows are returned (Sunday through Thursday), each with human-readable `start`/`end` day-and-time strings plus integer `startSecond`/`endSecond` giving seconds-of-week offsets for machine parsing. + +## price + +Always shell-routed. A bare positional symbol does not work outside the shell; use `price --symbol= --account= -e -q`. Success payload: `{"symbolName","bid","ask","spread","high","low","open"}`. An unknown symbol returns exit 1 with `Error: Symbol not found: NOTASYMBOL. Use 'symbols' to list available symbols.` — note this wording differs from the `candles` unknown-symbol message below; do not assume one canonical unknown-symbol string across market-data commands. + +## prices + +Always shell-routed. Call it `prices --symbols=,,... --account= -e -q`. Unlike `price` and `candles`, this plural form degrades per symbol inside a still-exit-0 response rather than hard-failing the whole call: `{"results":[{"symbolName":"EURUSD","ok":true,"quote":{...}},{"symbolName":"NOTASYMBOL","ok":false,"error":"Symbol not found: NOTASYMBOL. Use 'symbols' to list available symbols."}]}`. When processing a multi-symbol batch, check the per-item `ok` field rather than relying on the overall exit code. + +## candles + +Always shell-routed. Use `candles --symbol= --period= --account= -e -q` plus either `--count=` or a `--from=`/`--to=` range. + +Count form payload: `{"symbolName","timeframe","requested","returned","available","bars":[...]}`. `available` reports the total cached bars for that symbol-and-period pair and varies by timeframe (for example 13999 for EURUSD m1 versus 1296 for D1). **The count form returns at most 1000 bars per call**: requesting `--count=10000` or `--count=1001` both yield `"requested":1000` with no warning. To retrieve more history, page backward or forward through repeated `--from`/`--to` windows; there is no cursor or offset parameter. + +Range form payload has a different shape, without `requested`/`available`: `{"symbolName","timeframe","from","to","returned","bars":[...]}`. Accepted date formats for `--from`/`--to`: `yyyy-MM-dd`, `dd/MM/yyyy` (day-first), and a bare ISO datetime without offset — all three are interpreted as UTC. Omitting `--to` defaults it to the exact invocation instant; the still-open current bar is excluded, so only fully closed bars come back. The `to` bound is inclusive (a 10:00-14:00 window returns exactly 5 hourly bars). A reversed range (`from` after `to`) is rejected at exit 1 with `Error: Invalid parameters: 'from' must be before 'to'.` A closed-market window (a weekend date range) returns exit 0 with `{"returned":0,"bars":[]}` — this, not an error, is how you distinguish "no data in this window" from a malformed request. + +**Result ordering is always oldest-first ascending**, in both the count and range forms; never assume newest-first. Daily (`D1`) bar timestamps are not midnight UTC — they carry the broker's daily session boundary (`21:00Z` during summer time, `22:00Z` during winter time), so deriving "which calendar day" a bar belongs to from the UTC date component alone will be off by the session-boundary offset. + +An unknown symbol returns exit 1 with `Error: Symbol not available: NOTASYMBOL` — worded differently from `price`'s `Symbol not found` message. An invalid period value returns exit 1 with `Error: Invalid timeframe: 'BADPERIOD'.`, a distinct message you can match separately from the symbol-not-found family. + +## orders + +Shell-routed only (no batch code path): the CLI's own `--help` lists `orders` only under interactive commands, never under BATCH MODE. Call it `orders --account= -e -q`. Omitting `-q` still exits 0 eventually under redirected stdin, but the JSON is followed by the full interactive command menu and a `Bye.` line — noise to skip past, not a failure signal. `--account` can be omitted for a cTID with exactly one linked account (the banner then reads `Using your only account: ...` instead of `Using account: ...`), but pass it explicitly for determinism. + +Populated payload: `{"orders": [{"id","symbolName","tradeSide","orderType","volume","volumeLots","targetPrice","limitPrice","slippagePips","currentPrice","stopLoss","stopLossPips","takeProfit","takeProfitPips","expiration","label","comment"}, ...]}`. Optional fields are `null` when not set (`limitPrice`, `slippagePips`, `currentPrice`, `stopLoss`, `stopLossPips`, `takeProfit`, `takeProfitPips`, `expiration`). + +**Empty-result shape:** `{"orders": []}`, exit 0. + +## positions + +Same routing and call shape as `orders`: `positions --account= -e -q`. + +**Empty-result shape:** `{"positions": []}`, exit 0 — the normal case on an account with no open trades. For a populated entry, expect naming by analogy with `orders`/`orders-history` and confirm the exact field set on first use. + +## order + +Shell-routed only. `order ` as a bare trailing positional does **not** work at launch time — that syntax is documented shell-prompt-only in `--commands`' own entry for this verb — and returns exit 1: `Error: 'order' is required. Provide it as a positional argument inside the shell, or pass --order= when launching the command.` The working non-interactive form is `order --order= --account= -e -q`. + +A known id returns the order object directly, unwrapped (the same field set as one `orders[]` array element, not wrapped under an `order` key and not array-wrapped): `{"id":312753167,"symbolName":"EURUSD", ...}`. + +**Unknown-id shape:** exit 1 with a plain-text, non-JSON line: `Order #999999999 not found.` Do not attempt to JSON-parse stdout unconditionally; check the exit code first, or treat a JSON-parse failure itself as the not-found signal. This lookup is scoped to currently active orders — `order`/`orders` do not search closed or historical orders; use `orders-history` for that. + +## position + +Shell-routed only, symmetric with `order`. Bare positional fails identically at launch; use `position --position= --account= -e -q`. + +**Unknown-id shape:** exit 1, plain text: `Position #999999999 not found.` Like `order`, this lookup is scoped to currently open positions, not closed history. + +## exposure + +Shell-routed only. Call it `exposure --account= -e -q`. The JSON root key is the plural `exposures`, even though the command itself is singular — do not assume the payload key mirrors the command name. + +**Empty-result shape:** `{"exposures": []}`, exit 0. For a populated entry, confirm the exact field set on first use. + +## orders-history + +Shell-routed only; no `` positional form exists for this verb (per `--commands`, only bare, ``, and ` ` forms are defined) — `deals` supports the symbol form but `orders-history` does not. **History subcommands apply only the flags that fit their positional template.** A bare trailing CLI positional (e.g. `orders-history 50` typed at launch) is not applied at all; use the named launch flags `--count=`, `--from=`, `--to=` instead, which the CLI translates internally into the positional template. An unsupported flag for this subcommand, such as `--symbol=`, is silently not applied rather than rejected: `orders-history --symbol=EURUSD --count=5` still runs, and its header line reads plain `orders-history 5` with no trace of the symbol filter. **The echoed header line is exactly how you confirm which filters actually took effect** — read it before trusting the query you think you sent, since `--commands` output alone defines which flags a given history subcommand's template accepts. + +Count-based call (`orders-history --count= --account= -e -q`) returns `{"requested","returned","lookbackDays","orders":[...]}` when the count covers every order in the lookback window, and adds an `available` key — `{"requested","returned","available","lookbackDays","orders":[...]}` — when more orders exist in the lookback window than were returned (for example `--count=3` against 4 matching orders returns `"available": 4`, while a call whose count covers every matching order, or an empty history, omits the key). Default `requested` is 100 when `--count` is omitted; `lookbackDays` reports the lookback window (30) and appears only in this count-based envelope. + +Range-based call (`orders-history --from= --to= --account= -e -q`) returns a different envelope: `{"from","to","returned","available","orders":[...]}` — no `requested`/`lookbackDays` here, and `available` is present unconditionally rather than only on truncation. Dates without an explicit offset are interpreted as UTC (`2026-08-01` becomes `"2026-08-01T00:00:00.000Z"`). + +Each order object in either envelope carries: `id`, `symbolName`, `tradeSide`, `orderType`, `status`, `volume`, `volumeLots`, `executedVolume`, `executedVolumeLots`, `targetPrice`, `executionPrice` (null when never filled), `stopLoss`, `takeProfit`, `positionId` (null when never filled, the cross-reference to a resulting position once filled), `openTime` (ISO-8601 UTC with a trailing `Z`), `closeTime` (null while still open), `expiration`, `label`, `comment`. A `--to` bound resolving to midnight of the current day can fail to exclude still-open orders opened later that same day; a range entirely outside the data window returns an empty result. If you need a strict upper bound on `openTime`, filter client-side rather than relying solely on `--to`. + +**Empty-result shape:** an empty `orders` array with the surrounding envelope keys still present (for example `returned: 0`), exit 0 — never an error. + +## deals + +Shell-routed only. Unlike `orders-history`, `deals` supports every positional combination per `--commands`: bare, ``, ``, ` `, ` `, ` `. As with `orders-history`, bare trailing positionals typed at launch (`deals 50`) are not applied — use `--count=`, `--symbol=`, `--from=`, `--to=` instead, and check the echoed header line (e.g. `deals EURUSD 2026-08-01 2026-08-05`) to confirm which filters took effect. + +**Empty-result shape:** count-only or bare calls return `{"deals": [], "count": 0}`; a range call adds echo keys, `{"from":"...","to":"...","deals":[],"count":0}` — the `from`/`to` keys appear only when a range was supplied. For a populated entry, confirm the exact field set against a real deal before scripting against it. + +## alerts + +Shell-routed only. Call it `alerts --account= -e -q`. + +**Empty-result shape:** `{"alerts": []}`, exit 0. + +Populated payload: `{"alerts": [{"id","symbolName","price","condition","conditionType","quoteType","message"}, ...]}`. `condition` is the human-facing string (`above`/`below`); `conditionType` is the internal enum name (`GreaterOrEqual`/`LessOrEqual`); `quoteType` reports the quote side (`Bid`); `message` is always present, an empty string when none was supplied at creation. + +## indicators + +Batch-style discovery call, shell-routed: `indicators -e -q`. This is the sole discovery mechanism for valid indicator names. Payload: `{"indicators":[{"title","kind"}, ...],"count":88}`. Of the 88 entries, 87 carry `"kind":"BuiltInIndicators"` and one (`"Bar Sample"`) carries `"kind":"Indicator"`. The `title` string is the exact token to pass as `--indicator=` to other indicator subcommands. + +## indicator parameters + +Call it `indicator parameters --indicator="" -e -q`, using the exact `title` string from `indicators`. A bare positional indicator name (even quoted) does not work: `indicator parameters "Simple Moving Average" -e` returns exit 1, `Error: 'indicator' is required. Provide it as a positional argument inside the shell, or pass --indicator=<value> when launching the command.` + +Success payload: `{"indicator":"<title>","parameters":[{"name","friendlyName","type","groupName","description","defaultValue"}, ...]}`. For Simple Moving Average, the three parameters are `Source` (type `DataSeries`, default `Close`), `Periods` (type `Integer`, default `14`), and `Shift` (type `Integer`, default `0`). + +The `name` field is the token to use in a `--ind-params=<name>=<value>,...` override on other indicator commands; resolve it here rather than trusting the CLI's own generic `--help` text, which illustrates the override syntax with a singular `Period=20` example — the real name for Simple Moving Average is plural `Periods`. Always call `indicator parameters --indicator=<title>` first to get the exact name for the indicator you are working with, since names vary per indicator. diff --git a/skills/ctrader-cli/references/errors-and-exit-codes.md b/skills/ctrader-cli/references/errors-and-exit-codes.md new file mode 100644 index 0000000..e60d179 --- /dev/null +++ b/skills/ctrader-cli/references/errors-and-exit-codes.md @@ -0,0 +1,105 @@ +# Errors and Exit Codes + +This is the failure reference for the cTrader CLI: the exit-code table, the distinct message shapes and the stream each arrives on, and the recovery action for each class. Treat the table as a branch key and the message text as the signal you actually act on -- the two are complementary, not interchangeable. + +## Exit-code table + +| Code | Meaning | Recovery action | +| ------ | --------- | ------------------ | +| 0 | Success | Proceed; a success payload never contains the substring `Error:` | +| 1 | Invalid usage, unrecognized flag, missing required parameter, file not found, an interactive-path validation error, or a pre-routing parse error | Read the printed usage block or the specific validation message and correct the argument; do not retry unchanged | +| 81 | Invalid cTrader ID or password, exact message `Invalid ctid or password` | Re-check the `CTID` value and the `PWD-FILE` path and content; do not retry with the same credentials | +| 82 | Account cannot be found, exact message `Account cannot be found. Use accounts command to list all trading accounts linked to cTrader ID` | Call `accounts` to enumerate the cTrader ID's linked account numbers, then retry with a valid one | +| 124 | The external `timeout` ended the process | Check for a completion signal that already arrived (see the `backtest` section below) before treating this as a failure; on `build`, which exits on its own once the compile finishes, a 124 means the timeout expired before completion — raise the timeout and retry | + +Do not expect a distinct nonzero code for an unknown symbol, an unknown period, or an empty data window. `candles` reaches its validation through the interactive pipeline, where invalid symbol and invalid period land on exit 1 with a specific message (see the worked examples below), while an out-of-range date window returns exit 0 with an empty `bars: []` array rather than a distinct "no data" code. For any nonzero exit code not listed in the table, branch on the message text using the message shapes below, not on the number; the CLI's `--help` and `--commands` output documents no exit codes. + +## The stream-echo rule + +Whether stderr carries a one-line echo of the reconstructed command line is a function of which pipeline the call reached, not of success or failure: + +- A call that reaches the batch pipeline (`accounts`, `symbols`, `periods`, and the other batch verbs) always prints that echo to stderr, on exit 0 as well as on exit 1, 81, or 82 -- for example `ctrader-cli.exe accounts -e` produces a 29-byte stderr echo on a successful run, and the same shape of echo appears on an unknown-account exit 82. +- A call that lands in the interactive pipeline never prints that echo, regardless of its own exit code. Both a successful `candles` call and a `candles` call that fails validation leave stderr empty. +- A pre-routing parse failure -- the parser rejects the arguments before a pipeline is even chosen -- also leaves stderr empty. An empty `--pwd-file=` value, for instance, produces the stdout message `Unable to determine destination for argument value: --pwd-file=` at exit 1 with zero stderr bytes. + +Use stderr's content, not its mere presence, as the cheap diagnostic for which pipeline you reached: a stderr line that reproduces the invocation's argv means the call reached the batch pipeline (`--version` also echoes its argv this way, though it short-circuits before any pipeline routing); empty stderr means either a pre-routing parse failure or a landing in the interactive pipeline, and the stdout content (an `Error:` line versus the interactive banner and menu) tells you which; any other non-empty stderr content comes from a non-batch path — a quick-mode credential error such as `Missing --ctid in non-interactive mode.`, or the unhandled exception trace of the message shapes below. + +## Five message shapes + +Five distinct message shapes cover the failure surface. Distinguish them by which stream carries the text and whether the command-line echo is present, not by exit code alone. + +**Batch-pipeline `Error:` with stderr echo.** The call reached the batch pipeline, and validation failed there. Stdout carries a message beginning `Error:`, often followed by a full usage block; stderr carries the one-line command echo. + +```text +$ ctrader-cli.exe accounts --this-flag-does-not-exist=1 -e </dev/null +stdout: Error: Parameter this-flag-does-not-exist is not allowed + [usage block follows] +stderr: ctrader-cli.exe accounts --this-flag-does-not-exist=1 -e +exit: 1 +``` + +The same shape carries the auth and account-lookup failures: `Invalid ctid or password` at exit 81, and `Account cannot be found. Use accounts command to list all trading accounts linked to cTrader ID` at exit 82, both on stdout with the matching stderr echo. + +**Interactive-pipeline `Error:` without stderr echo.** The call reached the interactive pipeline, and validation failed inside it. Stdout carries an `Error:` message (sometimes after a banner and header line); stderr is empty. + +```text +$ ctrader-cli.exe candles --account=5816091 --symbol=NOTAREALSYMBOL --period=D1 --count=10 -q -e </dev/null +stdout: [interactive banner] + Error: Symbol not available: NOTAREALSYMBOL +stderr: (empty) +exit: 1 +``` + +An invalid timeframe on the same command produces the analogous shape: stdout `Error: Invalid timeframe: 'zz9'.`, empty stderr, exit 1. + +**Pre-routing parser message without echo.** The argument parser rejects the invocation before any pipeline is chosen. Stdout carries a message that does not begin with `Error:`; stderr is empty. + +```text +$ ctrader-cli.exe accounts --ctid="<id>" --pwd-file="" </dev/null +stdout: Unable to determine destination for argument value: --pwd-file= +stderr: (empty) +exit: 1 +``` + +The same shape appears when a positional argument itself cannot be resolved -- for example a nonexistent path passed to `metadata` produces `Unable to determine destination for argument value: <path>` on stdout with empty stderr, at exit 1, whether or not `-e` is present. Because this shape carries no `Error:` prefix, detect it by checking whether stdout is non-JSON and lacks that prefix, rather than by string-matching `Error:`. + +**Unhandled exception trace without echo.** An argument combination the parser cannot resolve at all -- most notably mixing a bare trailing positional (`yes`) with `--flag=value` syntax on a direct launch line -- ends the process with an unhandled exception trace instead of a clean message. Stdout is typically empty; the trace, naming `ConsoleInvalidUsageException` and the parser's own call path, arrives on stderr; exit is 1. + +```text +$ ctrader-cli.exe order cancel --order=312753167 yes --account=5816091 -e </dev/null +stdout: (empty) +stderr: Interactive shell crashed: ConsoleInvalidUsageException ... + [stack trace through the CLI's own parser classes] +exit: 1 +``` + +Pass confirmation with `-q` at launch instead of a trailing `yes` token to stay inside the first or second message shape rather than this one. + +**Shell-routed credential failure with banner-only stdout.** An invocation that routes into the shell pipeline without credentials — a dual-mode verb called with `-q` but without `-e` or explicit `--ctid`/`--pwd-file`, or a subcommand `--help` on a shell-routed verb — fails before any command validation. Stdout carries only the `cTrader CLI` banner line; stderr carries the plain-English message `Missing --ctid in non-interactive mode.` with no command echo, no `Error:` prefix, and no stack trace; exit is 1 (`periods -q`, `symbols -q`, `metadata <path> -q`, and `deals --help` all fail this way). Recover by supplying credentials (`-e` with `CTID`/`PWD-FILE` set, or explicit `--ctid`/`--pwd-file`), by dropping `-q` for a verb whose batch form needs no credentials (`periods`), or by using the top-level `ctrader-cli.exe --help`, the only help form that works without credentials. + +A non-numeric value supplied for a numeric flag stays inside the interactive-pipeline `Error:` shape rather than this exception-trace shape: `candles --account=<n> --symbol=EURUSD --period=D1 --count=notanumber -q -e` returns the message `Error: Option --count has invalid number: notanumber` on stdout, with stderr empty and exit 1. Validate numeric flag values before invoking, and read the named flag out of the message text to correct the call. + +## Build: exit code is not the completion signal + +`build` returns exit 0 for both a successful compile and a failed one. The process exit code carries no pass/fail information for this verb at all. The JSON body's `success` boolean is the only authoritative signal, with diagnostics in `errors: [{file, line, column, code, text}]` using Roslyn-style codes such as `CS1002` and `CS0103` when `success` is `false`. Read `success` from the JSON body on every `build` call regardless of exit code. + +## Backtest: exit code is not the completion signal + +`backtest` continues running after it has already printed its own completion output. A run prints a full progress log followed by a compact JSON summary on stdout -- keys such as `Equity`, `NetProfit`, `WinningTrades`, `LosingTrades`, `TotalTrades` -- within seconds of real work, and then keeps the process alive until the external `timeout` ends it at exit 124. When `--report-json` is supplied, that file is written correctly and completely even though the process itself has not yet been terminated. Treat the appearance of the final compact JSON summary on stdout, or the appearance of the `--report-json` file, as the completion signal for `backtest`; a 124 exit alongside either artifact is a completed run, not a failure. Always wrap `backtest` in an external `timeout` so the process is reliably ended once that signal has arrived. + +## Wrapper shells and reported exit codes + +A wrapper shell sitting between you and the CLI process can report a different exit code than the CLI itself produced -- for example a PowerShell call-operator invocation can surface the underlying failure through its own error-wrapping rather than passing the CLI's numeric code through untouched. When the message text and the exit code you observe disagree, trust the message text: it is the primary detection signal, and the exit code is the branch key you use only after the message has told you which branch you are on. + +## Recovery actions by class + +- **Exit 0, `success: false` or empty result payload (`build`, `candles` no-data window):** read the JSON body's own status field or content; this is not a process failure. +- **Exit 1, `Error:` on stdout, stderr echo present:** the batch pipeline rejected the arguments or a batch-routed lookup failed; correct the flag or value named in the message and retry. +- **Exit 1, `Error:` on stdout, stderr empty:** the interactive pipeline rejected the arguments; the message names the specific symbol, timeframe, or value at fault. +- **Exit 1, non-`Error:`-prefixed message, stderr empty:** a pre-routing parse failure; the message names the argument value the parser could not resolve. +- **Exit 1, empty stdout, exception trace on stderr:** an unhandled parser exception, typically from mixing a trailing bare positional with flag-style arguments; relaunch using `-q` for confirmation instead. +- **Exit 1, stdout `cTrader CLI` banner only, `Missing --ctid in non-interactive mode.` on stderr:** the call shell-routed without credentials; add `-e` or explicit credential flags, or drop `-q` where the batch form needs no credentials. +- **Exit 81:** re-check `CTID` and `PWD-FILE` before retrying. +- **Exit 82:** call `accounts` to get a valid account number before retrying. +- **Exit 124 on `backtest`:** check for the completion artifact (the final compact JSON summary on stdout or the `--report-json` file) before treating this as a failure; `backtest` keeps the process alive after completing its work, so a 124 with the artifact present is a completed run. +- **Exit 124 on `build`:** the timeout expired before the compile finished — `build` exits on its own (exit 0) once done, so no completion artifact will be present; raise the timeout and retry. diff --git a/skills/ctrader-cli/references/routing-and-output.md b/skills/ctrader-cli/references/routing-and-output.md new file mode 100644 index 0000000..cfcb506 --- /dev/null +++ b/skills/ctrader-cli/references/routing-and-output.md @@ -0,0 +1,114 @@ +# Routing and Output + +The pipeline a call lands in — batch or shell-routed — determines the payload casing, the wrapper key, and the accepted flag set. Below: the authoritative verb-by-verb routing matrix, the exact payload anatomy of each pipeline, an extraction recipe that holds up against banner variance, and the piped-stdin technique for folding several read-only calls into one process. + +## The batch verb list, authoritative + +`ctrader-cli.exe --help` carries a dedicated `BATCH MODE` section that is the primary source for which verbs have a genuine non-interactive code path. Its intro names six: `periods`, `accounts`, `symbols`, `metadata`, `run`, `backtest`. Two more verbs, `create` and `build`, are listed in the same section, but they execute through the command shell in every invocation shape; call them with `-q` (see the matrix below). Every other verb — including `account`, `account-stats`, `symbol`, `sessions`, `price`, `prices`, `candles`, `orders`, `order`, `positions`, `position`, `exposure`, `orders-history`, `deals`, `alerts`, `alert`, `indicators`, `indicator` — is absent from that section and appears only under the interactive command list, which means it is shell-routed by design rather than by an accidental flag combination. `ctrader-cli.exe --commands` prints that same interactive command reference on its own, grouped by category, at zero cost: no credentials, no network call, and a clean exit 0. + +Two verb families are worth distinguishing carefully because they are easy to conflate: + +- **Dual-mode verbs** (`accounts`, `symbols`, `periods`, and by the same BATCH MODE listing `metadata`, `run`, `backtest`) have a real batch code path and can also be pushed into the shell pipeline by adding `-q`. These are the verbs where "routing" is a live choice you make per call. `create` and `build` are not dual-mode despite their BATCH MODE listing: they route through the command shell in every invocation shape. +- **Shell-only verbs** (everything else) have no batch code path at all. Flags cannot select a batch shape for them because none exists; every invocation routes to the shell pipeline regardless of what you pass. The banner always appears; the `[timestamp] <command>` header appears once the command actually executes (it is absent when a pre-execution validation error fires, such as a missing required flag); and the payload is camelCase JSON on success but plain-text error lines on failure — so never JSON-parse shell-routed stdout unconditionally; check the exit code first, or treat a JSON parse failure as the error signal. + +## What rerouting actually changes + +Adding `-q` to a dual-mode batch verb does not just suppress a prompt. It moves the entire invocation onto a different code path, and that one flag changes four things simultaneously: + +1. **A banner and header appear.** Batch output starts at byte zero with `[` or `{`; shell-routed output is preceded by login lines and a `[timestamp] <command>` echo line. +2. **JSON casing flips.** Batch payloads are flat PascalCase (`Id`, `Number`, `Balance`); shell-routed payloads are camelCase (`traderLogin`, `balance`, `brokerName`). +3. **The payload gets wrapped.** Batch `accounts` returns a bare array; shell-routed `accounts -q` wraps the same concept under a command-name key: `{"accounts": [...]}`. +4. **The accepted flag set can change.** Batch `accounts` rejects `--account` outright (`Error: Parameter account is not allowed`); the same verb shell-routed via `-q` accepts `--account` to preselect an account. Batch `symbols` returns only `Id`, `Name`, `Description` per entry; shell-routed `symbols -q` swaps the schema rather than extending it — it gains `category` and `assetClass` but drops the numeric `Id` entirely, so neither shape is a superset of the other, and an agent that needs symbol ids must use the batch form. + +`periods` is the sharpest illustration of consequence four: its batch form takes zero flags, and passing even `-e` to it is itself a validation error (`Error: Parameter e is not allowed`). Passing `-q` to `periods` reroutes it into the shell, where it then demands credentials it never needed in batch form (`Missing --ctid in non-interactive mode.` when `-e` is also absent), and even with `-e -q` supplied it prints the full interactive menu rather than period data, because "periods" does not match any interactive command title. + +## The complete routing matrix + +| Verb | Batch code path exists | Call it | Payload shape | +| ------ | ------------------------ | --------- | --------------- | +| `periods` | yes, and it is the only route | bare, no flags at all | plain text, space-separated tokens, no credentials | +| `accounts` | yes | without `-q` | flat PascalCase JSON array | +| `symbols` | yes | without `-q`, with `--account=<n>` | flat PascalCase JSON array, `Id`/`Name`/`Description` only | +| `metadata` | yes | without `-q`, positional `.algo` path | PascalCase JSON object (`Name`, `Type`, `AccessRights`, `BuildTime`, plus a nested `Parameters` array whose entries carry `PropertyName`, `FriendlyName`, `Type`, `DefaultValue`) | +| `create` | no — listed under BATCH MODE, but it routes through the command shell and requires full account authentication | `create cbot --name=<name> -e -q` (a positional name at launch is not parsed and drops into the interactive menu) | banner + header, then a camelCase JSON object (`{kind, name, language, projectPath, projectFile, mainFile, expectedAlgoFile, status}`) | +| `build` | no — same BATCH MODE listing, same shell routing and authentication | `build --project-path=<path> -e -q` | banner + header, then a camelCase JSON object (`{projectPath, success, errorCode, errors, warnings}`); exit 0 on success and failure alike — read the `success` boolean | +| `run` | yes | without `-q` | plain-text streamed cBot output until the bot exits or the process is terminated; no final JSON summary — confirm the exact output shape on first use | +| `backtest` | yes | without `-q` | plain-text progress log plus a final compact PascalCase JSON summary; `--report-json` file is camelCase | +| `account`, `account-stats`, `symbol`, `sessions` | no | `-q` recommended | camelCase, banner + header, wrapped or bare object depending on verb | +| `price`, `prices`, `candles` | no | `-q` recommended, named `--symbol=`/`--symbols=` flag required | camelCase, banner + header | +| `orders`, `positions` | no | `-q` recommended | camelCase, wrapped under the command name (`{"orders": [...]}`, `{"positions": [...]}`) | +| `order`, `position` | no | `-q` recommended, `--order=`/`--position=` flag required | camelCase, bare unwrapped object for a known id (same field set as one array element from the list form) | +| `exposure` | no | `-q` recommended | camelCase, wrapped under the plural key `exposures` even though the command is singular (`{"exposures": [...]}`) — an agent must not assume the wrapper key mirrors the command name | +| `deals` | no | `-q` recommended, `--symbol=`/`--count=`/`--from=`/`--to=` flags | camelCase envelope: the array under `deals` plus `count`, with `from`/`to` echo keys in range form | +| `orders-history` | no | `-q` recommended, `--count=` or `--from=`/`--to=` flags only — no symbol form exists, and a `--symbol=` passed anyway is silently discarded (exit 0, results unfiltered) | camelCase envelope with the array under `orders`, not the command name: `{requested, returned, lookbackDays, orders}` in count form (plus `available` when results were truncated), `{from, to, returned, available, orders}` in range form | +| `alerts`, `alert` | no | `-q` recommended | camelCase, `{"alerts": [...]}` or the single-entity response | +| `indicators`, `indicator parameters` | no | `-q` recommended | camelCase | + +For most "no batch code path" rows, the bare positional form that would feel natural (`price EURUSD`, `order 312753167`) drops into the interactive shell and fails with a message that names the fix — `price`, `symbol`, `order`, `alert`, and `indicator parameters` all fail this way, for example: `'symbol' is required. Provide it as a positional argument inside the shell, or pass --symbol=<value> when launching the command.` The named flag form at launch time is the one that works outside the live REPL. A few rows diverge from that failure pattern and exit 0 instead: `account <n>` appears to accept its bare positional, but the number is silently discarded and the payload is the default account's — a bogus number returns the same output — while `deals` and `orders-history` silently drop a trailing positional (`deals 50` returns the same payload as bare `deals`; `orders-history 50` returns the default `"requested": 100`). Exit 0 on those rows never proves the positional was applied; pass the named flags at launch instead. + +`-q` is recommended rather than mandatory for these shell-only reads: a call without `-q` but with stdin redirected from `/dev/null` still terminates cleanly. It runs the requested command, falls through to the interactive menu once, hits end of stream, prints `Bye.`, and exits 0. The only cost of omitting `-q` here is the noise of that trailing menu dump landing in your captured stdout; `-q` skips the menu entirely and keeps stdout minimal for parsing. + +An unrecognized verb token follows the same fall-through path: it does not produce an invalid-usage error, it silently routes to the interactive menu and exits 0 once stdin closes. Exit 0 on its own is therefore never proof that the verb you intended actually ran; confirm from the payload shape or the header echo line, not from the exit code alone. + +## Batch payload anatomy + +A true batch call has no preamble of any kind. Stdout begins at byte zero with the JSON structure itself: + +```json +[{"Id":47246474,"Number":5816091,"Broker":"Spotware","Live":false,"DepositCurrency":"EUR","Leverage":100,"Balance":1000.0}] +``` + +There is no timestamp header line, no banner, no trailing menu. Stderr, in this pipeline, carries exactly one thing: the CLI's own echo of the invoked command line, present on success and on failure alike, for example `ctrader-cli.exe accounts -e`. That echo's presence is itself a signal that you reached the batch pipeline, not a diagnostic to react to. + +## Shell-routed payload anatomy + +A shell-routed call carries four layers in this order, and the exact wording of the first layer is not fixed: + +1. **Banner and login lines.** Wording varies by account state: `Connecting as <id>...`, `Logged in`, `Using account: #5816091 Spotware EUR 1000 demo`, or, when the cTID has exactly one linked account and `--account` was omitted, `Using your only account: #5816091 Spotware EUR 1000 demo`. `Connected as <id>...` can also appear. Transient `Connection failed ... Retrying` lines may precede the banner on a flaky connection; treat any such line as wording variance to scan past, not as a signal about payload success. Redact the cTrader ID (shown here as an email/login, distinct from the account number that follows it in the same sentence) when quoting captured banner text into a report or any shared artifact. +2. **A timestamp header line**, of the form `[yyyy-MM-dd HH:mm:ss <local numeric offset>]` followed by the command as invoked, for example `[2026-08-03 16:12:44 +03:00] price EURUSD`. This is local wall-clock time carrying the machine's own zone offset; it is not the format used for timestamps inside the JSON body, which are UTC with a `Z` suffix (`2026-07-01T21:00:00.000Z`). Treat the header as machine-local and the body's timestamps as UTC; do not assume they share an epoch reference. +3. **The camelCase JSON payload**, wrapped under a key that matches the command name, for example `{"orders": [...]}`, `{"accounts": [...]}`, `{"alerts": []}`. A handful of single-entity shell-routed commands (`account`, `symbol --symbol=<name>`, `order --order=<id>`, `position --position=<id>`) return the object bare rather than wrapped, and `exposure` wraps under the plural key `exposures` rather than the singular verb name, while `orders-history` wraps its array under `orders` — the same key the `orders` command uses — inside a count-based or range-based envelope, so discriminate a history payload from an open-orders payload by the envelope keys or the header echo line, never by the wrapper key alone. Check the verb's own documented shape rather than assuming the wrapper key mirrors the command name. +4. **The interactive menu and `Bye.`**, present only when `-q` was omitted. This is a full listing of every interactive command and exits the process at end of stdin; it is not an error state and should simply be excluded from parsing, which `-q` avoids by not printing it at all. + +Because banner wording varies and can include retry lines, never locate the payload by a fixed line count or byte offset. Scan structurally instead. + +## A robust payload-extraction recipe + +Locate the payload by content, not position, so that banner wording variance, retry lines, or an unexpectedly long login sequence never break the extraction: + +```bash +timeout 30 ctrader-cli.exe orders -e -q --account=<n> </dev/null 1>out.txt 2>err.txt +echo "EXIT:$?" +``` + +Then extract with one of these approaches, in order of preference: + +- **Batch payloads:** the file already is the payload; parse it directly as JSON from byte zero. +- **Shell-routed payloads:** find the first line that starts with `{` or `[` and treat everything from there to the matching closing brace or bracket as the JSON body. In practice the timestamp header line is the reliable anchor: the payload begins on the next non-blank line after the `[yyyy-MM-dd HH:mm:ss <offset>] <command>` line. + +```bash +awk '/^\[[0-9]{4}-[0-9]{2}-[0-9]{2} /{found=1; next} found && NF{print; exit}' out.txt +``` + +That prints the first non-blank line after the header, which is where the JSON body starts; feed the remainder of the file from that point into your JSON parser rather than assuming a fixed number of preceding lines. Never assume the banner is a fixed number of lines: it varies with account count, connection retries, and which login-state messages the CLI chose to print for that session. + +## The piped-stdin multi-command technique + +Every fresh process costs roughly 1.5 to 2 seconds of start-up. When you need several shell-capable reads in the same turn, fold them into a single process by piping multiple command lines to one launch instead of paying that start-up cost per call: + +```bash +printf 'accounts\nsymbols\nq\n' | timeout 60 ctrader-cli.exe -e --account=<n> +``` + +This authenticates once, then runs each line as a command in turn, and `q` ends the session cleanly. Two commands folded into one piped session complete in 4.1 to 4.4 seconds, versus 5.1 to 5.8 seconds for the same two commands issued as separate cold-start batch calls — a saving of roughly 1.0 to 1.7 seconds (20 to 25 percent) for the second command, consistent with the stated per-process start-up budget. Blank lines and lines beginning with `#` inside the piped script are ignored, so a multi-line script can carry comments for readability. + +The shell-shape caveat is the detail most worth internalizing: a piped session is a shell session end to end. Its output arrives in shell-routed shape — one banner and login sequence at the top, then camelCase JSON wrapped under each command's own key, one payload per line you fed in — never in batch PascalCase, even for verbs like `accounts` and `symbols` that would be flat PascalCase if called individually without `-q`. Parse every payload from a piped session as camelCase and expect the command-name wrapper, and expect exactly one banner regardless of how many commands you folded in, not one banner per command. + +## Casing and field-shape differences worth checking before you parse + +Casing follows the pipeline, not the verb, so the same conceptual field can appear under two different names depending on how you called it: + +- Batch `accounts` returns `Id`, `Number`, `Broker`, `Live`, `DepositCurrency`, `Leverage`, `Balance`. Shell-routed `accounts -q` returns `traderLogin`, `brokerName`, `depositCurrency`, `environment`, `isCurrent`, `balance`, `accountName`, `leverage`, `accountType`, `accessRights`, `accountStatus`, `isSwapFree`, `isLimitedRisk`, `traderId` — more fields, not just renamed ones. +- Batch `symbols` returns `Id`, `Name`, `Description` per entry — hundreds of entries, tens of kilobytes, on a typical broker. Shell-routed `symbols -q` swaps the field set per entry — `name`, `description`, `category`, `assetClass`, with no id field in any casing — and is correspondingly larger; the numeric `Id` exists only in the batch shape. Filter by name or use the single-symbol detail command rather than loading the full array into context. +- `backtest` is the one verb where the split runs the other direction within a single call: its own compact stdout summary prints PascalCase (`Equity`, `NetProfit`, `ProfitFactor`), while the file it writes for `--report-json` is camelCase (`main`, `equity`, `tradeStatistics`). Read whichever one you need directly; do not assume the two share a casing convention just because they come from the same invocation. `build` has no report flags at all, and its stdout JSON (`projectPath`, `success`, `errors`, `warnings`) is camelCase. + +Numbers throughout are plain JSON numbers with no locale-dependent thousands separators — shell-routed payloads arrive already rounded by the CLI's own formatting (money to 2 decimal places, pips to 1, ratios and percentages to 2) — with one exception: a backtest that closes zero trades renders `AverageTrade` and `ProfitFactor` in its compact stdout summary as a bare unquoted `-` token, which is not valid strict JSON; special-case that token before parsing. Line endings on Windows are CRLF; account for that when splitting captured output into lines. diff --git a/skills/ctrader-cli/references/setup.md b/skills/ctrader-cli/references/setup.md new file mode 100644 index 0000000..1537793 --- /dev/null +++ b/skills/ctrader-cli/references/setup.md @@ -0,0 +1,167 @@ +# Session preflight and setup guidance + +Run this four-step preflight before the first authenticated call in a session, and whenever an authenticated call fails in a way that looks credential-related rather than usage-related. +Each step reports presence and length only; never print or log the value of `CTID` or `PWD-FILE`, and never echo the contents of the password file. + +The CLI runs natively on every platform. On Windows it installs with winget as `ctrader-cli.exe`; on macOS and Linux it installs with Homebrew from the Spotware tap as `ctrader-cli` — the same CLI with identical verbs, flags, and environment variables. Each step below gives both shapes where they differ; where only one shape appears, drop the `.exe` suffix on macOS and Linux. + +## Step 1: executable reachability + +Confirm the CLI is runnable and report its version. + +Windows (native install): + +```bash +command -v ctrader-cli.exe +timeout 30 ctrader-cli.exe --version </dev/null +``` + +`command -v` exits 0 with the full path to the executable (for example `C:\Users\<you>\AppData\Local\Programs\cTrader CLI\ctrader-cli.exe`). +`--version` exits 0 with stdout `Version: 5.9.0.38` (or later); stderr echoes the invoked command line, which is benign and appears on every call this preflight prescribes (all batch-routed), success or failure — interactive-routed invocations and pre-routing argument-parse failures omit the echo. +`--version` accepts `-e` without complaint even though it has no matching option to resolve: it short-circuits before option validation, unlike most other verbs. +On Windows the output uses CRLF line endings, so a parser that strips only `\n` from captured `--version` output leaves a trailing `\r` in the extracted version string; strip both. + +macOS and Linux (Homebrew install): + +```bash +command -v ctrader-cli +timeout 30 ctrader-cli --version </dev/null +``` + +If neither shape works, the CLI is not reachable yet. + +On macOS, GNU `timeout` is not preinstalled; it comes with coreutils (`brew install coreutils`). For these short read-only probes, running without the `timeout` wrapper is an acceptable fallback until coreutils is installed. + +## Step 2: credential presence and length + +Check for `CTID` and `PWD-FILE` using a quoted argument to `printenv`, captured through command substitution, never a raw pipe into `wc -c` and never direct `$VARNAME` interpolation. +`PWD-FILE` contains a hyphen, which makes it an illegal bash identifier: `$PWD-FILE` does not raise an error, it silently expands `$PWD` (the current directory) followed by the literal text `-FILE`, with no error to signal the mistake. +A raw `printenv "PWD-FILE" | wc -c` pipe over-counts the true length by one, because `printenv` appends a trailing newline that `wc -c` counts along with the value. +The correct idiom captures the value into a variable first, then measures the shell's own length operator: + +```bash +val="$(printenv 'CTID')"; echo "CTID: ${#val} characters" +val="$(printenv 'PWD-FILE')"; echo "PWD-FILE: ${#val} characters" +``` + +These idioms work identically in Git Bash on Windows and in any POSIX shell on macOS or Linux. +An empty result (`0 characters`, or the variable failing to capture anything) means the variable is not set in this session. Report presence and length only, for example "CTID: present, 22 characters" or "CTID: absent" -- never the value itself. + +On macOS and Linux, `PWD-FILE` is commonly absent from the shell environment even in a correctly configured setup, because a hyphenated name cannot be assigned with plain `export` in a POSIX shell. That absence is not a failure: the Step 4 probe supplies the password-file path per invocation instead (an explicit `--pwd-file` flag, or an `env "PWD-FILE=..."` prefix). On those platforms, Step 2 therefore checks `CTID` only, and Step 3 checks the password file at its conventional path. + +## Step 3: password-file sanity + +Confirm the file at the `PWD-FILE` path exists and that its first line has nonzero length, again reporting presence and length only, never content: + +```bash +val="$(printenv 'PWD-FILE')" +val="${val:-$HOME/.ctrader/pass.pwd}" +if [ -f "$val" ]; then + first_line_len=$(head -n 1 "$val" | tr -d '\r\n' | wc -c) + echo "Password file exists; first line length: $first_line_len" +else + echo "Password file not found at the configured PWD-FILE path" +fi +``` + +The password file's first line is the password itself; the CLI reads only that first line, so trailing lines are not consulted. + +## Step 4: the go/no-go probe + +Run the single authoritative probe with stdin redirected, and read the exit code directly rather than through a pipe. + +Windows (native install): + +```bash +timeout 30 ctrader-cli.exe accounts -e </dev/null; echo "EXIT:$?" +``` + +macOS and Linux — `-e` resolves `CTID` from the environment while the password-file path is passed explicitly (an explicit flag always wins over the matching variable, so the mix is well-defined): + +```bash +timeout 30 ctrader-cli accounts -e --pwd-file="$HOME/.ctrader/pass.pwd" </dev/null; echo "EXIT:$?" +``` + +The alternative shape, which keeps the command line identical to the Windows one, supplies `PWD-FILE` through `env` (which accepts hyphenated names that plain `export` cannot set): + +```bash +env "PWD-FILE=$HOME/.ctrader/pass.pwd" timeout 30 ctrader-cli accounts -e </dev/null; echo "EXIT:$?" +``` + +Four outcomes are possible: + +- **Exit 0, stdout a JSON array of account objects.** The CLI is ready for authenticated calls. Report the account number(s) and each account's `Live` field (demo vs live) back to the user; never assume a fresh session is a demo account without checking `Live`. +- **Exit 81, stdout `Invalid ctid or password`.** The CTID value or the password file's content is wrong. Tell the user their credentials were rejected and ask them to re-check the email address in `CTID` and the password text in the file at `PWD-FILE` (first line only, no extra whitespace or trailing newline pasted in by an editor). +- **Exit 1, stdout `Error: Password file not found` plus a usage block.** The configured password-file path points at a file that does not exist. Ask the user to confirm the path in `PWD-FILE` (or the one passed to `--pwd-file`) matches an actual file. +- **Exit 1, stdout a missing-parameter message (for example `Error: Should be specified parameter: --pwd-file`) plus a usage block.** A fast, deterministic failure, never a hang: `-e` was omitted from the command line, or the variables are absent from this terminal's environment. A session or license cache can let `accounts -e` succeed even when the two variables are absent, so re-run Step 2 to confirm both variables are actually set in the current terminal before looking further. + +Always redirect stdin, bound the call with the `timeout` wrapper, and always read `$?` directly, so a misconfigured session fails fast instead of leaving the agent waiting on a prompt that will never be answered. + +## User-facing remediation text + +Use this wording verbatim (adjusting only the account details and paths) when a step above indicates the user needs to act. Every command below is copy-pasteable as written; give the user the block that matches their OS. + +If Step 1 fails on Windows: + +> cTrader CLI is not installed or not on PATH. Install it with `winget install Spotware.cTrader.CLI`, then open a new terminal so PATH updates take effect. + +If Step 1 fails on macOS or Linux: + +> Install the cTrader CLI with Homebrew: +> +> ```text +> brew tap spotware/tap https://github.com/spotware/homebrew-tap +> brew install spotware/tap/ctrader-cli +> ``` +> +> This puts the `ctrader-cli` executable on your PATH; it is the same CLI with the same commands and flags. + +If `CTID` is absent in Step 2, on Windows: + +> Set your cTrader ID (the email address you log in with) as a persistent environment variable: +> +> ```text +> setx CTID "you@example.com" +> ``` +> +> Then open a new terminal. + +If `CTID` is absent in Step 2, on macOS or Linux: + +> Add your cTrader ID (the email address you log in with) to your shell profile (`~/.zshrc` or `~/.bashrc`): +> +> ```text +> export CTID="you@example.com" +> ``` +> +> Then open a new shell, or run `source` on the profile file. + +If `PWD-FILE` is absent in Step 2, or Step 3 reports the file missing, on Windows: + +> Create a plain text file whose first line is your cTrader password, for example `C:\Users\<you>\.ctrader\pass.pwd`, then set: +> +> ```text +> setx PWD-FILE "C:\Users\<you>\.ctrader\pass.pwd" +> ``` +> +> Then open a new terminal. + +If Step 3 reports the file missing, on macOS or Linux: + +> Create a plain text file whose first line is your cTrader password, at `~/.ctrader/pass.pwd`. No environment variable is needed for it: a hyphenated name like `PWD-FILE` cannot be set with plain `export`, so pass the path per invocation instead — either `--pwd-file="$HOME/.ctrader/pass.pwd"` on the command line, or an `env "PWD-FILE=$HOME/.ctrader/pass.pwd"` prefix (the Step 4 probe shows both shapes). + +If Step 4 returns exit 81: + +> Your credentials were rejected. Re-check the email address in `CTID` and the password text in the file at `PWD-FILE` (first line only, no extra whitespace). + +If Step 4 returns the missing-parameter branch after Steps 2 and 3 both looked correct: + +> Open a fresh terminal (variables set with `setx` on Windows, or added to a shell profile on macOS/Linux, only take effect in sessions started afterwards) and re-run the probe in Step 4. + +The canonical credential flag is `--pwd-file`; `--password-file` is not part of the option set and returns a usage message rather than being accepted as an alias, so never suggest it to a user or use it in a scripted invocation. `--password` belongs to interactive use and should not appear in a scripted invocation either. + +## Why a new terminal is required + +Persistent environment variables take effect only in sessions started after they were written: `setx` on Windows updates the registry-backed value without touching any running terminal, and a profile `export` on macOS or Linux is only read when a new shell starts (or the profile is explicitly sourced). +Every existing shell keeps the environment it started with until it is closed and a new one is opened, which is why each remediation message above that sets a persistent variable ends with "open a new terminal" before retrying. +An agent that runs the Step 4 probe in the same terminal session where the user just configured the variable will see the same failure as before, even though the value is now set correctly for future sessions; this is expected, and the fix is a new terminal, not a different probe command. diff --git a/skills/ctrader-cli/references/trading-write.md b/skills/ctrader-cli/references/trading-write.md new file mode 100644 index 0000000..bf0a0ae --- /dev/null +++ b/skills/ctrader-cli/references/trading-write.md @@ -0,0 +1,107 @@ +# Trading write reference + +This reference covers every state-changing verb: `order place-market`, `order place-limit`, `order place-stop`, `order place-stop-limit`, `order modify`, `order cancel`, `position close`, `position close-partial`, `position modify`, `alert create`, and `alert delete`. +The confirmation mechanism, the volume semantics, and the three-state protection rule apply across this whole set, and each works differently from what habit built on the read-only verbs would predict. + +## The authorization gate + +On every command in the mutating set, `-q` is not a routine menu-skip convenience the way it is for a read. It **is** the confirmation. Passing it answers the confirmation prompt automatically and the mutation executes immediately, with no further chance to abort. + +Because `-q` is also the flag you reach for reflexively on shell-routed reads to keep stdout minimal, the single most important discipline in this reference is: never let it travel from a read command to a write command out of habit. Add `-q` to a mutating command only when both of the following are true: + +- The user has explicitly authorized this specific action. Not authorization to trade in general, and not authorization given earlier in the conversation for a different order or a different symbol; the specific action you are about to submit. +- You have confirmed the target account is a demo account by reading the account's own record from `accounts` and checking that it reports `"Live": false`. Do this check freshly before the mutation, not from memory of an earlier read in the same session. + +Never run a mutating command to discover how it behaves. If you need to understand a flag's shape or a response's fields, read `--commands`, this reference, or a neighboring read-only command instead of a live trial mutation. + +## The confirmation mechanism + +Only `-q` reliably confirms a mutating command at a direct, flag-style launch invocation (the shape you use: explicit flags, `--account=`, stdin redirected from `/dev/null`). The other forms that the CLI's own refusal message names do not work the way that message implies, in this invocation shape: + +- `--yes` and `-y` both produce the identical refusal text, followed by `Cancelled.`, at exit 1, exactly as if no confirmation flag had been passed at all — the same refusal on `order place-limit`, `order modify`, and `order cancel` alike. The refusal reads: + +```text +Refused: confirmation required. Append `yes` as a trailing positional inside the shell, or pass --yes/-q at launch time. +Cancelled. +``` + +- A bare trailing `yes` positional, mixed into an otherwise flag-style command line, does not confirm and does not cleanly refuse either. It ends the process with an unhandled `ConsoleInvalidUsageException` stack trace on stderr (empty stdout, exit 1). The trailing-`yes`-positional convention belongs to typing at the interactive shell's own `>` prompt; it is not something you can append to a launch line built of `--flag=value` arguments. +- The `all` keyword (for bulk operations such as `alert delete --alert=all` or a bare `all` positional) is shell-prompt syntax in the same sense. At a direct launch invocation, `--alert=all` is rejected with `Error: Alert id must be a number, got 'all'.`, and a bare `all` positional is rejected with `Error: 'alert' is required. Provide it as a positional argument inside the shell, or pass --alert=<value> when launching the command.` Note that `--help` also documents a dedicated `--all` launch flag (`target every applicable entity`, covering `stop`, `order cancel`, `position close`, and `alert delete`); prefer explicit-ID deletion or cancellation until you have verified `--all` yourself. + +So: `-q` at launch is the confirmation form that applies to the invocations you compose. Of the other forms, only the bare trailing `yes` positional ends the process with the usage-trace exception; `--yes` and `-y` fail gracefully with the refusal text plus `Cancelled.`, and the `all` spellings (`--alert=all` and a bare `all` positional) fail gracefully with a one-line `Error:`. The trailing `yes` and bare `all` keywords are the interactive shell's own `>`-prompt syntax; `--yes`/`-y` are launch options per the CLI's own `--help` (`--yes, -y skip confirmations`) that do not confirm in this invocation shape — never rely on them. + +Expect the same `-q`-confirms / `--yes`-and-`-y`-refuse pattern on every verb in the mutating set: `--commands` shows each one exposing the identical confirmation convention, listing trailing-`yes` positional forms alongside its argument shapes. Re-confirm on first use of a verb where precision matters. + +## The four-stage guarded sequence + +Drive every mutation through this four-stage sequence, in order, every time: + +1. **Pre-flight read.** Before submitting anything, read the instrument's own tradeable limits with `symbol --symbol=<name>` (returns `lotSize`, `minVolume`, `maxVolume`, `volumeStep`, `digits`, `pipSize`, current `bid`/`ask`), and read the current state of the entity you are about to mutate (`orders`, `positions`, or `alerts`) so you know its prior values. +2. **Act.** Submit the mutating command with `-q`, after the authorization gate above has been satisfied. +3. **Read back.** Immediately re-read the affected entity with `orders`, `positions`, `orders-history`, or `alerts` as appropriate. +4. **Verify.** Compare the read-back against what you expected the mutation to have produced (the new price, the new stop, the removed order, the deleted alert) before you report success to the user. A command's own confirmation JSON is not a substitute for this independent read-back. + +## Volume is expressed in units by default + +`--volume` is interpreted in units unless you say otherwise. On EURUSD (`lotSize=100000`, `minVolume=1000`, `volumeStep=1000`), these three invocations of `order place-limit` resolve to the identical stored order: + +- `--volume=1000` with no `--volume-type` flag at all +- `--volume=1000 --volume-type=units` +- `--volume=0.01 --volume-type=lots` + +Each reads back as `volume: 1000, volumeLots: 0.01`. The confirmation text printed at submission time for the untyped case reads `BUY 1000 units (= 0.01 lots)`, spelling out the resolution in the same call. + +Always read the instrument's limits first with `symbol --symbol=<name>` before choosing a volume, since `minVolume`, `maxVolume`, and `volumeStep` are per-instrument and a value outside them is what you are validating against, not a fixed constant. + +## Stop loss and take profit: the three-state rule + +`order modify` treats `--sl` and `--tp` as three-state flags; expect the same from `position modify` and confirm on first use: + +- Passing a value replaces the field with that value. +- Passing `0` removes the field (read back as `null`). +- Omitting the flag entirely preserves whatever value is already set; it does not clear it. + +Modifying with `--sl=0` prints confirmation text `SL=remove` and reads back as `stopLoss: null`, while a `takeProfit` omitted from the same call comes back unchanged. A call with `--sl=1.0250` (a plain value) replaces the field. Supply prices as absolute values, not as pip offsets. + +## Wrong-side rejection + +A stop loss or take profit on the wrong side of the reference price is rejected client-side, before the request ever reaches the server, so no state changes. The exact shape, for a buy limit order at 1.0378 modified with `--sl=1.0500`: + +```text +Warning: SL=1.0500 is on the profit side of reference price 1.0378 for buy (server may reject). +Error: SL/TP is on the wrong side of the reference price. The server would silently drop the protection. Modification rejected. +``` + +Exit code 1, two lines of plain text: a `Warning:` line naming the specific side violation, then an `Error:` line stating the rejection. The order's protection is left exactly as it was; treat this rejection as a safe no-op, and still confirm with a read-back rather than an assumption. + +## Flag names that matter + +Use the exact flag names below. An unrecognized near-miss is treated as if the value were never supplied, so the result is a missing-value message naming the option the command still needs rather than a message about the name you typed: + +- `--order=<id>` identifies the target for `order modify` and `order cancel`. `--order-id=<id>` is not recognized: it produces `Error: 'order' is required. Provide it as a positional argument inside the shell, or pass --order=<value> when launching the command.`, which reads like a missing value rather than a wrong flag name. +- `--position=<id>` identifies the target for `position close`, `position close-partial`, and `position modify`, following the same naming convention as `--order`. +- `--alert=<id>` identifies the target for `alert delete`. +- `--symbol=<name>` must be passed as an explicit named flag everywhere a symbol is required, including `order place-*` and `alert create`. A bare positional symbol is not consumed at a direct launch invocation; it reroutes into the interactive shell instead. + +## Response keys per command + +Each mutating command returns its own identifier key and its own `status` string; read the key that matches the command you actually called rather than assuming a single conceptual identifier name across the set: + +| Command | Identifier key | `status` value | +| --- | --- | --- | +| `order place-limit` | `orderId` | `"placed"` | +| `order modify` | `orderId` | `"modified"` | +| `alert create` | `id` | `"created"` | +| `alert delete` | `alertId` | `"deleted"` | + +The `id` versus `alertId` naming difference between the two alert commands is the CLI's convention to read from, not an inconsistency to work around. For the placement verbs beyond the table, expect `{"orderId": <int>, "status": "placed"}` when the submission rests as a pending order and `{"positionId": <int>, "status": "opened"}` when it fills into a position immediately — the normal outcome for `place-market` — and read whichever of the two keys is present rather than assuming `orderId`. For `order cancel`, `position close`, `position close-partial`, and `position modify`, expect `orderId` (cancel) or `positionId` (the position commands, with `close-partial` also carrying `volume` and `volumeLots` for the partial amount); confirm the exact key on first use before relying on it in a script. + +## Alerts: create and delete + +`alert create` requires the flag form at launch: `--symbol=`, `--price=`, and `--condition=` (accepted values `above` and `below`); bare positionals are not consumed at a direct launch invocation. The confirmation text echoes the alert in plain language, for example `Create alert: EURUSD below 0.5.`, before the JSON payload. + +A populated `alerts` read returns an object keyed `alerts`, each entry carrying `id`, `symbolName`, `price`, `condition` (the human-facing string, `above`/`below`), `conditionType` (the internal enum name, `GreaterOrEqual`/`LessOrEqual`), `quoteType`, and `message` (an empty string when none was supplied at creation). An account with no alerts returns `{"alerts": []}`. + +`alert delete` takes `--alert=<id>` and returns the `alertId`/`"deleted"` pair described above. Delete by explicit numeric ID; `--alert=all` and a bare `all` positional are both rejected at launch (see the confirmation section above); `--help` documents a separate `--all` launch flag for bulk deletion — verify it before relying on it. + +Follow the four-stage sequence for alerts exactly as for orders: read `alerts` before creating (to know the starting state), create with `-q` after authorization, read `alerts` back to confirm the new entry's fields match what you intended, and after a delete, read `alerts` again to confirm the entry is gone rather than trusting the delete command's own JSON alone.