Deployment
ops/ has one launchd plist per profile plus ops/install.sh, which is the
supported install path — it creates the logs/ and data/ directories launchd
cannot create for itself, and refuses to install an env file that still holds
template placeholders or a port already in use.
| Unit | Profile | Port | Env file |
|---|---|---|---|
com.ctrader-markets.forex | forex | 8010 | .env.forex |
com.ctrader-markets.deriv | deriv | 8011 | .env.deriv |
Run exactly one process per broker account. Two processes on the same credentials mean two sessions and two token-refresh races.
Install a profile
python3.12 -m venv .venv
.venv/bin/python -m pip install -e '.[dev]'
cp .env.example.forex .env.forex # then fill it in
./ops/install.sh forexops/install.sh is the supported path because it does the things whose absence
is invisible:
- creates
logs/anddata/, which are gitignored and absent from a fresh checkout. launchd creates the log file but not its parent directory, so without this the redirect silently fails and a crash loop leaves no trace. - refuses to install while the env file still holds
replace-with-…placeholders. - refuses a port already in use.
- refuses two profiles sharing a
TOKEN_CACHE_PATH.
The plists hardcode absolute paths, because launchd expands neither ~ nor a
shell environment. Edit them if the checkout moves.
Health
curl -s localhost:8010/health/ready | jq .details/health/live answers as soon as the process is up. /health/ready returns 503
until the broker session is connected and a quote has arrived within
TICK_STALENESS_SECONDS, and its details carry state, reconnects and
last_error.
tail -f logs/forex.log # stdout, JSON per line
jq -r .event logs/events.forex.jsonl | sort | uniq -c # the durable recordWatch for ctrader_connect_failed, access_token_rejected,
access_token_invalidated and stream_subscriber_lagging.
Restart and removal
launchctl kickstart -k gui/$(id -u)/com.ctrader-markets.forex # restart
launchctl print gui/$(id -u)/com.ctrader-markets.forex # state, exit code
launchctl bootout gui/$(id -u)/com.ctrader-markets.forex # stop and unload
rm ~/Library/LaunchAgents/com.ctrader-markets.forex.plist # uninstallA restart is always safe: the service re-authenticates, reloads the symbol catalog and re-subscribes from configuration alone. Nothing is resumed from disk except the token pair.
Failure modes
| Symptom | Likely cause | Check |
|---|---|---|
logs/forex.log empty, process restarting every 60s | crash before logging — bad env file, port in use, missing venv | run .venv/bin/ctrader-markets --profile forex in the foreground |
/health/ready 503, last_error mentions CH_CLIENT_AUTH_FAILURE | wrong CTRADER_CLIENT_ID / CTRADER_CLIENT_SECRET | re-check the application page at openapi.ctrader.com |
503 with symbol_resolution_failed in the log | a name in SYMBOLS is not exposed by this broker | --discover-symbols, copy exact symbolName values |
repeated access_token_rejected then silence | refresh token expired or already rotated elsewhere | redo the OAuth flow, then --refresh-token |
| ready but no ticks on a weekday | symbols resolve but the market is closed, or the account has no feed | compare against a cTrader chart |
stream_subscriber_lagging in the events log | an SSE consumer is too slow; oldest ticks are being dropped for it | the dropped counter on that subscriber’s status events |
ThrottleInterval of 60s stops a crash loop from hammering the broker. Reconnect
backoff grows to RECONNECT_MAX_BACKOFF_SECONDS and only resets once a
connection has stayed up for RECONNECT_STABILITY_SECONDS.
Nothing rotates logs/*.log; they grow unbounded. Add a newsyslog.d drop-in
with size-based rotation and the J (compress) flag — launchd holds the file
open, so do not move files out from under the process. See ops/README.md.