Logo hamster-bot Claude new: MCP
RU EN PT

Loading…

HTTP API

HTTP API for controlling the bot and the tester: settings, strategies, exchange API keys, positions and orders, logs, running the tester and working with its reports. Address — the same as the web interface: http://<host>:<port>, the port is set by the port field in settings_program.json.

91 endpoints

Access#

API token (recommended)#

Send the Authorization: Bearer <api_token> header with every request:

curl -H "Authorization: Bearer <api_token>" http://127.0.0.1:5000/api/current/bots

The bot creates api_token itself on first start and stores it in settings_program.json. The token works from any address and with any auth_telegram value, and a browser login does not reset it. Keep the token like a password: it gives access to everything, including trading and exchange keys.

You can view and copy the token in the web interface: “Global configuration”, “API and MCP token” block. The “Issue new” button is there too; the old token stops working immediately. With Telegram login disabled, this block only works from the same machine (127.0.0.1). In the config editor, in the log and in the GET /api/settings/config response the token is hidden (the last 4 characters are visible), and it cannot be changed through the editor or POST /api/settings/config.

A new token via the API: POST /api/settings/api-token/regenerate, response { "success": true, "api_token": "..." }.

Without a token, from the same machine#

If Telegram login is disabled ("auth_telegram": false), requests without a token are accepted only from the same machine at 127.0.0.1 or localhost:

curl http://127.0.0.1:5000/api/current/bots

From other addresses without a token the API returns 401. With login disabled the web interface is still open to anyone who can connect to the port, so on a server with an open port keep login enabled.

Requests from pages of other sites (POST with an Origin header that does not match the bot's address) are rejected with 403 — this is CSRF protection. curl, scripts and MCP clients do not send the Origin header, so this does not affect them. If the bot is behind a reverse proxy, the proxy must pass the original Host header.

Requests and responses#

  • The body of POST requests is JSON in UTF-8 (Content-Type: application/json).
  • Names in the URL path are URL-encoded: strategy T4 BTC → /api/settings/strategy/T4%20BTC.
  • Settings and configs are sent and returned in the same form as in the bot's JSON files.
  • A successful action returns { "success": true, ... }.
  • An error returns { "success": false, "error": "error text" } with a code from the Response codes table.
  • A bot is identified by the account and the strategy name: /{accountName}/{settingsName}. If a strategy trades several pairs, the ?symbol= parameter selects the pair. Without it the first one is used.

Settings#

GET/api/settings/config#

The global program config (settings_program.json). The Telegram bot token is masked: the first 4 characters are visible. api_token is masked: the last 4 characters are visible.

200 Response
{
  "name": "MyBot",
  "use_web": true,
  "port": 5000,
  "api_token": "••••••••••••b9c2",
  "telegram": {
    "bot_token": "1234************",
    "id_chat": 123456789
  }
}
Errors
  • 503the config has not been loaded yet
POST/api/settings/config#

Changes the global config and applies it immediately. Send only the fields that change — the rest stay as they were. The Telegram bot token, api_token and tester_data_dir are not changed here: these fields in the body are ignored.

Request
{ "name": "MyBot", "chart": { "count": 500 } }
200 Response
{ "success": true }
Errors
  • 400empty body or invalid JSON
  • 500failed to save
POST/api/settings/api-token/regenerate#

Issues a new api_token; the old one stops working immediately. This is the only API response where the token is shown in full. No body needed.

200 Response
{ "success": true, "api_token": "<new token>" }
Errors
  • 500failed to save
GET/api/settings/hints/{kind}#

Descriptions of settings fields — the same hints the settings editor shows. {kind}:

  • settings — strategy settings (bot and tester), for all strategies at once;
  • config_tester — the tester config, including parameter_mining, walk_forward and the reports_history.csv columns;
  • settings_program — the global config.

Parameters:

  • ?lang= — en (default), ru or pt;
  • ?filter= — only fields whose path contains one of the comma-separated substrings, for example ?filter=zigzag,close_by_margin.
200 Response
an object “dotted field path → description”:
{
  "parameter_mining": "List of parameters to mine. Each has a name (parameter path) and either a start/end/step range or a values list...",
  "monte_carlo": "Mining limit: if there are more combinations than this number, a random sample of this size is used..."
}
Errors
  • 404unknown {kind}
GET/api/settings/schema#

Which settings fields actually have an effect. ?strategy= — strategy code (basic.strategy); without it — a map for all strategies.

200 Response ?strategy=zz, abridged
{
  "strategy": "zz", "type": "trend",
  "sections": ["zz"],
  "ignored_sections": ["hd", "zigzag", "mrs2", "..."],
  "position_filter": true, "supported_in_tester": true,
  "conditional_fields": [
    { "fields": ["zz.depth", "zz.detection", "zz.open", "..."], "active_when": "zz.type = ZZ2" },
    { "fields": ["zz.zz6_*"], "active_when": "zz.type = ZZ6" },
    { "fields": ["zz.ma_filter"], "active_when": "zz.use_ma_filter = true" },
    { "fields": ["basic.balance_percentage_long", "basic.risk_long"], "active_when": "basic.use_long = true and basic.lot_long = 0" }
  ],
  "notes": { "zz.zz6_risk_max": "ZZ6 only caps the lot multiplier ..." },
  "tester_ignores": { "basic.leverage": "the tester does not model leverage ..." },
  "shared_fields": ["zz.from_level_price"]
}
  • sections — sections the strategy reads; it does not read sections from ignored_sections. Common sections (basic, options, filter, exchange) are not listed.
  • position_filter — the strategy respects filter; filter.<name>.* fields take effect only when filter.<name>.use = true.
  • conditional_fields — fields that take effect only under a condition. A trailing * means all fields with this prefix.
  • notes — fields that affect the result only in edge cases (mining them often gives identical rows).
  • tester_ignores — fields the tester does not model.
  • shared_fields — section fields that other strategies read too.

A field without a rule is considered effective. The same rules produce the warnings when saving settings and starting the tester.

Errors
  • 404unknown strategy

Strategies#

GET/api/settings/strategy#

All bot strategies.

200 Response
[
  {
    "name": "T4 BTC",
    "is_runing": true,
    "exchange": { "name": "binance", "account": "main" },
    "basic": { "symbol": "BTCUSDT", "strategy": "zigzag", "time_frame": "1h" }
  }
]
GET/api/settings/strategy/{name}#

One strategy by name (case-insensitive).

Errors
  • 404not found
POST/api/settings/strategy/draft#

A strategy draft with default values for the selected account. Saves nothing: edit the result and send it to POST /api/settings/strategy.

Request
{ "account": "main", "symbol": "BTCUSDT", "strategy": "zigzag", "time_frame": "1h", "account_type": "UNIFIED" }

Instead of symbol you can send symbols: ["BTCUSDT", "ETHUSDT"] — a strategy on several pairs. time_frame (default 1h) and account_type are optional.

200 Response
a strategy object
Errors
  • 400strategy or pair not specified
  • 404account not found
POST/api/settings/strategy#

Creates or changes a strategy. The body is a strategy object, as in GET /api/settings/strategy/{name}. Changes apply to running bots immediately, a new strategy starts immediately.

?old_name= — rename the strategy old_name to the name from the body.

Request
POST /api/settings/strategy?old_name=T4%20BTC
200 Response
{ "success": true, "name": "T4 BTC v2", "created": false, "warnings": null }

created: true — the strategy is new. warnings — changed fields the strategy does not read (see /api/settings/schema); null — no remarks.

Errors
  • 400name, exchange.name, exchange.account or basic.strategy are empty, or the name contains invalid characters
  • 500failed to save
POST/api/settings/strategy/{name}/copy#

Copies a strategy under a new name, saves and starts the copy.

Request
{ "new_name": "T4 ETH", "new_symbol": "ETHUSDT" }

new_symbol is optional: without it the pair stays the same as in the source strategy.

200 Response
{ "success": true, "name": "T4 ETH" }
Errors
  • 404source strategy not found
  • 409a strategy with this name already exists
POST/api/settings/strategy/from-tester#

Creates a bot strategy from verified tester settings. The settings are copied as is; exchange.name and exchange.account come from the account. By default the strategy is created paused (is_runing: false): the bot loads it but does not trade until it is started (play).

Request
{ "folder": "lut_now", "name": "T_BTC", "account": "demo_bybit", "new_name": "T_BTC live", "start": false }
  • folder — tester settings folder, see Settings folder in a request;
  • new_name — strategy name; without it — the same as the settings;
  • start: true — start trading immediately.
200 Response
{ "success": true, "name": "T_BTC live", "account": "demo_bybit", "exchange": "bybit", "is_runing": false, "warnings": null }

warnings — as when saving a strategy, plus a warning if the settings were tested on a different exchange than the account's.

Errors
  • 404tester settings or account not found
  • 409a strategy with this name already exists
  • 500failed to save
POST/api/settings/strategy/{name}/delete#

Deletes a strategy and stops its bots. Orders and the position on the exchange are left untouched.

?full=1 — full removal: the strategy is paused, all its orders are cancelled, the position is closed, and only then the strategy is deleted. If cancelling orders or closing the position fails, the strategy stays paused and is not deleted — the position will not be left unmanaged.

How to switch a strategy off correctly. While paused, the position and orders stay on the exchange without the strategy managing them (its stops, take-profits and exits), so they must not be abandoned. The order:

  1. pause;
  2. cancel orders;
  3. close the position;
  4. delete the settings.

?full=1 does exactly this: after the pause it waits for the bot's current cycle to finish so it cannot place a new order. By hand — the same order, and the pause must not be skipped: a running bot places orders again right after the cancellation.

Request
POST /api/settings/strategy/T4%20BTC/delete?full=1
200 Response
{ "success": true }
Errors
  • 404not found
  • 409for full=1 the strategy must be running
  • 500failed to cancel orders, close the position or delete the strategy

Accounts#

An account is an exchange API key that strategies trade with.

GET/api/settings/accounts#

All accounts.

200 Response
[
  {
    "name": "main",
    "exchange": "binance",
    "api_key": "...",
    "api_secret": "...",
    "is_encrypted": true
  }
]
GET/api/settings/accounts/{name}#

One account by name (case-insensitive).

Errors
  • 404not found
GET/api/settings/accounts/{name}/symbols#

Pairs available on the exchange for this account.

200 Response
["BTCUSDT", "ETHUSDT"]
Errors
  • 404account not found
POST/api/settings/accounts#

Creates or changes an account. Changes apply to running bots immediately.

?old_name= — rename the account old_name to the name from the body.

Keys are sent in plain text. With "is_encrypted": true the bot stores them encrypted. Empty api_key, api_secret, api_passphrase keep the current values on change; when changing is_encrypted the key and secret must be sent again.

Request
{ "name": "main", "exchange": "binance", "api_key": "...", "api_secret": "...", "is_encrypted": true, "open_positions_limiter": 0 }
200 Response
{ "success": true, "name": "main", "applied": 3 }

applied — how many running bots received the new key.

Errors
  • 400name or exchange not specified, invalid name, or keys missing when changing is_encrypted
  • 500failed to save
POST/api/settings/accounts/{name}/delete#

Deletes an account.

200 Response
{ "success": true }
Errors
  • 404not found
  • 500failed to delete
GET/api/current/accounts/{name}/info#

Account and key data from the exchange (updated hourly). At least one running bot on this account is required.

200 Response
{
  "name": "main",
  "exchange": "binance",
  "api_key": "abc1****",
  "account_type": "UNIFIED",
  "symbol": "BTCUSDT",
  "ok": true,
  "status": "OK",
  "warnings": [],
  "rows": [ { "name": "Permissions", "value": "..." } ],
  "updated_utc": "2026-10-25T12:00:00Z"
}
Errors
  • 404account not found
  • 409no running bot on the account
GET/api/current/accounts/{name}/balance-history#

Account balance history. ?max= — at most this many points (default 5000).

200 Response
{
  "account": "main",
  "total": 12000,
  "points": [ { "time": 1761393600, "margin": 1234.5, "wallet": 1200.0, "positions": "..." } ]
}

time — Unix time in seconds, total — how many points the history has in total.

POST/api/current/accounts/{name}/balance-history/compact#

Compacts the balance history now using the monitoring settings (this normally happens automatically).

200 Response
{ "success": true, "message": "...", "points_before": 12000, "points_after": 3000, "bytes_before": 512000, "bytes_after": 128000 }

Bots#

A bot is a running strategy on a specific pair.

GET/api/current/bots#

Summary of all bots.

200 Response
[
  {
    "account": "main",
    "exchange": "binance",
    "strategy": "T4 BTC",
    "strategy_code": "zigzag",
    "symbol": "BTCUSDT",
    "time_frame": "1h",
    "is_runing": true,
    "is_live": true,
    "status_text": "...",
    "last_price": 65500.0,
    "last_bar_time": "2026-10-25T12:00:00",
    "last_sync_utc": "2026-10-25T12:00:05Z",
    "sync_missed_cycles": 0,
    "position": { "side": "Buy", "size": 0.01, "entry_price": 65000.0, "cost": 650.0, "unrealized_pnl": 5.0 },
    "orders": 2
  }
]

is_runing: false — the bot is paused. position: null — no position. orders — number of open orders. sync_missed_cycles — how many cycles in a row failed to get data from the exchange.

GET/api/current/balances#

Wallet and margin balance for each API key, separately for spot and futures. ?account= — only this account.

Request
GET /api/current/balances?account=main
200 Response
[
  { "account": "main", "exchange": "bybit", "type": "futures", "wallet": 1234.56, "margin": 1180.20 },
  { "account": "main", "exchange": "bybit", "type": "spot", "wallet": 500.00, "margin": 500.00 }
]

wallet — wallet balance, margin — margin balance, as the exchange returns them. For spot both values are the USDT balance. The balance comes from running bots: an account without a running bot is not listed.

GET/api/current/bots/{accountName}/{settingsName}/bars#

Bot candles. ?limit= — the last N candles (default 500).

Request
GET /api/current/bots/main/T4%20BTC/bars?limit=100
200 Response
an array of candles with the fields Timestamp, Open, High, Low, Close, Volume and others.
Errors
  • 404bot not found
GET/api/current/ubots#

A full snapshot of the state of all bots. The field set is detailed and may change between versions — for a permanent integration use GET /api/current/bots, /api/current/positions and /api/current/orders.

GET/api/current/ubots/{accountName}/{settingsName}#

A full snapshot of the state of one bot (see the note above).

Errors
  • 404bot not found
POST/api/bots/pause-all#

Pauses all bots.

200 Response
{ "success": true, "count": 5 }
POST/api/bots/play-all#

Starts all bots.

200 Response
{ "success": true, "count": 5 }
POST/api/bots/{accountName}/{settingsName}/pause#

Pauses a strategy (for a multi-pair strategy — all its pairs): it stops placing orders. The open position and orders stay on the exchange without the strategy managing them — after pausing either start it again or cancel the orders and close the position (see deletion).

Request
POST /api/bots/main/T4%20BTC/pause
200 Response
{ "success": true, "name": "T4 BTC", "is_runing": false }
Errors
  • 404not found
  • 500failed to save
POST/api/bots/{accountName}/{settingsName}/play#

Starts a paused strategy.

200 Response
{ "success": true, "name": "T4 BTC", "is_runing": true }
Errors
  • 404not found
  • 500failed to save
POST/api/bots/{accountName}/{settingsName}/send-chart#

Sends the bot's chart to Telegram.

200 Response
{ "success": true }
Errors
  • 404bot not found
  • 500failed to build or send the chart
POST/api/bots/{accountName}/{settingsName}/send-position#

Sends a bot summary to Telegram: balance, position, PnL, orders.

200 Response
{ "success": true }
Errors
  • 404bot not found
POST/api/delete/{accountName}/{settingsName}#

Deletes the bot's strategy and removes the bot from operation. Orders and the position on the exchange are not removed and stay unmanaged. For a safe shutdown use POST /api/settings/strategy/{name}/delete?full=1.

Request
POST /api/delete/main/T4%20BTC
200 Response
{ "success": true }
Errors
  • 404bot not found
  • 500failed to delete

Positions and orders#

GET/api/current/positions#

Positions of all bots.

200 Response
[
  {
    "account": "main",
    "exchange": "binance",
    "strategy": "T4 BTC",
    "symbol": "BTCUSDT",
    "position": {
      "Symbol": "BTCUSDT",
      "Side": "Buy",
      "Size": 0.01,
      "EntryPrice": 65000.0,
      "Cost": 650.0,
      "UnrealizedPnl": 12.5,
      "Leverage": 10,
      "Cross": false
    }
  },
  {
    "account": "sub1",
    "exchange": "binance",
    "strategy": "T3 ETH",
    "symbol": "ETHUSDT",
    "position": null
  }
]
GET/api/current/orders#

Open orders of all bots.

200 Response
[
  {
    "account": "main",
    "exchange": "binance",
    "strategy": "T4 BTC",
    "symbol": "BTCUSDT",
    "order": {
      "Id": "order-123",
      "client_id": "cl-456",
      "Market": "BTCUSDT",
      "Side": "Buy",
      "Type": "Limit",
      "Status": "New",
      "Size": 0.01,
      "filledSize": 0.0,
      "remainingSize": 0.01,
      "Price": 64000.0,
      "TriggerPrice": null,
      "ReduceOnly": false
    }
  }
]
POST/api/trading/market#

A market order on the account. The pair must be traded by one of this account's bots.

Request
{ "account": "main", "symbol": "BTCUSDT", "side": "Buy", "size": 0.01 }

side — Buy or Sell; size — volume in the exchange's units for this pair.

200 Response
{ "success": true, "order_id": "order-123" }
Errors
  • 400invalid side or size
  • 404the account has no bot with this pair
  • 500the exchange rejected the order
POST/api/positions/{accountName}/{settingsName}/close#

Closes the bot's position.

Request
POST /api/positions/main/T4%20BTC/close
200 Response
{ "success": true }
Errors
  • 404bot not found
POST/api/positions/{accountName}/close-all#

Closes all open positions of the account.

200 Response
{ "success": true, "count": 3 }

count — how many positions were sent for closing.

POST/api/positions/{accountName}/reduce#

Reduces all open positions of the account by the given percentage.

Request
{ "percent": 50 }

percent — from 1 to 100.

200 Response
{ "success": true, "count": 3 }
POST/api/orders/{accountName}/{settingsName}/{orderId}/cancel#

Cancels one bot order (regular or stop order).

Request
POST /api/orders/main/T4%20BTC/order-123/cancel
200 Response
{ "success": true }
Errors
  • 404order not found
  • 500the exchange did not cancel the order
POST/api/orders/{accountName}/cancel-all#

Cancels all orders of the account.

200 Response
{ "success": true, "count": 2 }

count — how many bots had their orders cancelled.

POST/api/orders/cancel-all#

Cancels all orders on all accounts.

200 Response
{ "success": true, "count": 7 }

System and logs#

GET/api/system/status#

Program version, server resources and counters. The fields pid, exe_path, working_directory, port (the port the request arrived on) and started_utc uniquely identify the process instance: an external script uses them to tell several running bots apart and to notice a restart (pid and started_utc change).

200 Response
{
  "program": "hamster-bot",
  "version": "2026/10/23",
  "name": "MyBot",
  "type": "bot",
  "os": "Linux 6.1",
  "architecture": "X64",
  "machine": "srv1",
  "runtime": ".NET 10.0",
  "processors": 4,
  "pid": 12345,
  "exe_path": "/opt/hamster-bot/hb_c",
  "working_directory": "/opt/hamster-bot",
  "port": 80,
  "started_utc": "2026-10-24T08:00:00Z",
  "uptime_sec": 86400,
  "cpu_percent": 12.5,
  "memory_total_bytes": 8589934592,
  "memory_available_bytes": 4294967296,
  "process_memory_bytes": 314572800,
  "disk": "/",
  "disk_total_bytes": 107374182400,
  "disk_free_bytes": 53687091200,
  "accounts": 2,
  "strategies": 5,
  "bots": 6,
  "bots_running": 5,
  "auto_update_supported": true
}
POST/api/system/shutdown#

Shuts down the program. Available only with Telegram login. The program does not start by itself: an external supervisor (service, systemd, script) restarts the process. You can check that the supervisor brings the bot back 61 seconds after a stop (RestartSec=61, run_service.cmd) with the tools/check-shutdown-restart.ps1 script (it stops the running bot). There is no restart_program tool. It is usually not needed: changes to strategies, accounts and settings through the API apply to running bots immediately. Update with restart — POST /api/system/update. After a restart, use pid and started_utc from /api/system/status to confirm that a new process came up. Before stopping, check that no tester run is in progress: it will be interrupted, and the queue is not saved.

200 Response
{ "success": true } — the program exits in half a second
Errors
  • 409Telegram login is not configured
POST/api/system/update#

Starts a program update.

200 Response
{ "success": true }
Errors
  • 409auto-update is not supported on this system (see auto_update_supported in /api/system/status)
  • 500the update did not start
GET/api/logs#

The last lines of the general log. ?tail= — how many last lines (default: all available).

200 Response
an array of strings
GET/api/logs/errors#

Errors for the last ?hours= hours (default 24, at most 48). ?warnings=1 — together with warnings. If Telegram is not configured (telegram.id_chat = 0), messages are not sent and there are no “chat not found” errors: the log gets the warning Telegram chat is not set once per run. A “chat not found” error with a chat set means a wrong chat id or token. Repeats are already grouped: identical messages from one source are merged into one entry, count — how many times it repeated over the whole stored window (up to 48 hours, since program start), first_utc and last_utc — the first and last occurrence. Entries go from newest to oldest.

?since= — UTC time (for example 2026-11-02T06:00:00Z; without a time zone it is treated as UTC): only entries that repeated after it are returned. To check “what is new since last time”, pass the latest last_utc from the previous response. If an entry's first_utc is earlier than since, it is an old error that happened again, and count stays cumulative. An invalid date — 400.

200 Response
[
  {
    "level": "Error",
    "first_utc": "2026-10-25T08:00:00Z",
    "last_utc": "2026-10-25T12:00:00Z",
    "count": 3,
    "source": "T4 BTC",
    "message": "...",
    "exception": "..."
  }
]
GET/api/logs/files#

Log files, newest first; each day is a new file. ?contains= — only files whose name contains the text (strategy name, account, pair).

Log types:

  • strategy log — log_<date>_<Exchange> <account> <pair> <timeframe> <strategy> <name>.log, one per bot: signals, orders, fills, position, strategy errors;
  • account log — log_<account>_<date>.log: balances and positions from the exchange, exchange API responses and errors, position limiter, close_by_margin;
  • general program log — log_<date>.log (/api/logs);
  • tester logs — TESTER_*.log.

The date format in the names varies, so it is easier to find a file with contains.

200 Response
[ { "name": "log_25-10-2026.log", "size": 102400, "modified_utc": "2026-10-25T12:00:00Z" } ]
GET/api/logs/files/{filename}#

Log file content as text. ?download=1 — download as a file.

Errors
  • 400invalid file name
  • 404file not found

Tester#

The tester runs strategies on historical data. Parameter mining, stress tests and snapshot replays run one at a time. Single runs can run in parallel. While historical data is downloading the tester does not start, and vice versa — in these cases the response is 409.

All runs work in the background: the response comes immediately, progress and results are in GET /api/tester/status.

GET/api/tester/status#

Tester state.

200 Response
{
  "run": {
    "active": true,
    "status": "Running",
    "started_utc": "2026-10-25T12:00:00Z",
    "finished_utc": null,
    "message": "API mining run",
    "runs_done": 40,
    "runs_total": 120,
    "current_bar": 5000,
    "total_bars": 20000,
    "parallel": false,
    "report_folders": ["T4"],
    "result_files": ["T4/reports_history.csv", "T4/report_optimizer_T4_auto_x_auto_y_20261025_120000.html", "T4/run_snapshot_T4_20261025_115500.json"]
  },
  "wizard_runs": [
    {
      "run_id": "a1b2c3d4",
      "strategies": ["T4 BTC"],
      "status": "Completed",
      "started_utc": "2026-10-25T11:00:00Z",
      "finished_utc": "2026-10-25T11:02:00Z",
      "current_bar": 20000,
      "total_bars": 20000,
      "message": "Completed: 1 strategies.",
      "report_path": "T4/T4_run_001_of_001_BTCUSDT_1h_ee8c3c2e.html",
      "report_json_path": null,
      "snapshot_path": "T4/run_snapshot_T4_20261025_110000.json",
      "stats": null,
      "warnings": []
    }
  ],
  "download": { "active": false, "status": "Idle", "started_utc": null, "finished_utc": null, "message": "", "result": null },
  "settings_folder": ""
}
  • run — parameter mining, a stress test or a snapshot replay. status: Idle, Running, Completed or Failed; runs_done / runs_total — how many parameter combinations have been calculated. report_folders — result folders of the current or last run inside root=report; result_files — final files already written to them since the run started: reports_history.csv, the summary HTML (report_optimizer_*, stress_test_*, walk_forward_*) and the snapshot.
  • queue — full-mode runs waiting their turn (?queue=1): queue_id, position, message, queued_utc.
  • wizard_runs — single runs. Finished ones stay visible for another hour. Metrics (stats) are not sent here — they are in /api/tester/wizard-runs/{run_id}. Batch runs that have not started yet have status: "Queued".
  • batches — batches of single runs: batch_id, done of total.
  • download — historical data download.
  • settings_folder — the current tester settings folder.
GET/api/tester/config#

The tester config (config_tester.json): period, fees, mining parameters and so on.

POST/api/tester/config#

Saves the tester config. The body is the whole config, as in GET /api/tester/config.

200 Response
{ "success": true }
Errors
  • 400empty body or invalid JSON
  • 500failed to save
GET/api/tester/data-coverage#

Which historical data is already downloaded: ?exchange= (required), ?symbols=A,B (without it — all downloaded pairs of the exchange), ?start=&end= — the period to check.

200 Response
[
  {
    "symbol": "ADAUSDT", "downloaded": true, "first_day": "2026-04-09", "last_day": "2026-09-15",
    "days": 67, "gap_days": 93, "size_mb": 1.78,
    "period": { "covered": false, "missing_days": 132, "missing_first": ["2026-03-01", "2026-03-02", "2026-03-03"], "missing_last": "2026-08-30" }
  },
  { "symbol": "NOPEUSDT", "downloaded": false }
]

gap_days — days without data between the first and the last. period is present only with start and end. Only downloaded files count: what is missing is downloaded by the run itself (UpdateData) or by download-data.

Errors
  • 400the exchange is not supported or only one of start / end is set
GET/api/tester/exchanges#

Exchanges the tester can download historical data for.

200 Response
["bybit", "binance", ...]
GET/api/tester/exchanges/{exchange}/symbols#

Pairs of the exchange. An empty list means the exchange is currently unavailable; the pair can be entered manually.

Errors
  • 404the tester does not support this exchange

Settings folder in a request#

All requests to tester settings, runs and data downloads accept a settings folder: ?folder= in the URL or the folder field in the run body. It is a path inside tester/settings_strategy, "" is the main folder. Without folder the folder selected in the interface is used (POST /api/tester/settings-folder). This is shared state: a user or a parallel task can change it, so scripts and assistants should always pass folder.

A non-existent folder is a 404 error. To create a folder — POST /api/tester/settings-folders.

Settings can have the optional fields tags (a list of labels) and owner (the author, for example "MCP"). They are used to filter settings in the list and to delete them in bulk.

GET/api/tester/settings#

Tester strategy settings in the folder. Parameters:

  • ?folder= — the folder;
  • ?mask= — only names matching the mask (* — any characters, ? — one character);
  • ?tag=, ?owner= — only with this label or this author;
  • ?full=1 — whole settings instead of the short form: a folder export that /api/tester/settings/import restores.
200 Response
[ { "name": "T4 BTC", "exchange": "bybit", "symbol": "BTCUSDT", "symbols": null, "strategy": "zigzag", "time_frame": "1h", "tags": ["tmp"], "owner": "MCP" } ]
GET/api/tester/settings/{name}#

The whole tester settings. ?folder= — the folder.

Errors
  • 404not found
POST/api/tester/settings/draft#

A tester settings draft with default values. Saves nothing.

Request
{ "exchange": "bybit", "symbol": "BTCUSDT", "strategy": "zigzag", "time_frame": "1h" }

Instead of symbol you can send symbols — several pairs. account_type is optional.

200 Response
a settings object
Errors
  • 400the tester does not support the exchange, or strategy or pair not specified
POST/api/tester/settings#

Saves tester settings. The body is a settings object. ?old_name= — rename the settings old_name. Existing settings with the same name are not overwritten. ?folder= — the folder. ?owner_default= — the owner value if the settings have none (MCP passes MCP).

200 Response
{ "success": true, "name": "T4 BTC", "folder": "", "warnings": null }

warnings — changed (non-default) fields that have no effect: a section of another strategy (one line per section), a field of another zz.type, a disabled use_* switch, basic.leverage (the tester does not model leverage) — see /api/settings/schema. null — no remarks. Warnings do not cancel the save.

The same warnings field is in the settings copy response, and the import has warnings by the name of each settings item.

Errors
  • 400required fields are empty
  • 409settings with this name already exist
  • 500failed to save
POST/api/tester/settings/{name}/copy#

Copies tester settings within the same folder (?folder=), optionally to another exchange or pair. ?owner_default= — as when saving. To copy to another folder — copy-to-folder.

Request
{ "new_name": "T4 BTC binance", "new_exchange": "binance", "new_symbol": "" }

Empty new_exchange and new_symbol keep the values of the source settings.

200 Response
{ "success": true, "name": "T4 BTC binance" }
Errors
  • 404source settings not found
  • 409the name is taken
POST/api/tester/settings/{name}/delete#

Deletes tester settings. ?folder= — the folder. To delete many settings at once — delete-matching.

200 Response
{ "success": true }
Errors
  • 404not found
GET/api/tester/settings-folder#

The current tester settings folder and its subfolders. Folders let you keep several sets of settings; "" is the main folder.

200 Response
{ "path": "", "folders": [ { "name": "2026-10", "settings": 12 } ] }

settings — how many settings the folder has.

POST/api/tester/settings-folder#

Selects the tester settings folder for the interface and for requests without folder.

Request
{ "path": "2026-10" }
200 Response
{ "success": true, "path": "2026-10" }
Errors
  • 404folder not found
POST/api/tester/settings-folders#

Creates a settings folder, including nested ones. Does not change the selected folder.

Request
{ "path": "research/zz_hedge" }
200 Response
{ "success": true, "path": "research/zz_hedge", "created": true }

created: false — the folder already existed.

Errors
  • 400empty path, .. or invalid characters in the name
POST/api/tester/settings-folders/delete#

Deletes a settings folder with all its contents. The main folder cannot be deleted. If the selected folder is deleted, the main one becomes selected.

Request
{ "path": "research/zz_hedge" }
200 Response
{ "success": true, "deleted_settings": 12 }
Errors
  • 404folder not found or it is the main folder
POST/api/tester/settings/copy-to-folder#

Copies settings from one folder to another.

Request
{ "from": "mrs-bybit", "to": "lut_now", "names": ["C_MU", "T_*"], "on_conflict": "replace" }

Which settings to copy — names (names or masks), mask ("*" — all), tag, owner; filters combine, at least one is required. on_conflict — what to do if the name already exists in the target folder: skip (default), replace or error (copy nothing).

200 Response
{ "success": true, "copied": ["C_MU"], "replaced": ["T_BTC"], "skipped": [] }
Errors
  • 400no filter, a name without a mask not found, the folders are the same, or a conflict with on_conflict: "error"
  • 404folder not found
POST/api/tester/settings/delete-matching#

Deletes folder settings by a filter.

Request
{ "folder": "lut_now", "owner": "MCP", "tag": "tmp" }

Filters — as in copy-to-folder, at least one is required.

200 Response
{ "success": true, "folder": "lut_now", "deleted": ["MCP_LZ1"], "not_found": [] }
Errors
  • 400no filter
  • 404folder not found
POST/api/tester/settings/import#

Saves a set of settings to a folder, for example an export from GET /api/tester/settings?full=1.

Request
{ "folder": "lut_now", "settings": [ { ... }, { ... } ], "on_conflict": "skip", "owner_default": "MCP" }
200 Response
{ "success": true, "folder": "lut_now", "saved": ["T_BTC"], "skipped": [], "warnings": { "T_BTC": ["..."] } }
Errors
  • 400invalid settings or a conflict with on_conflict: "error"
  • 404folder not found
GET/api/tester/settings-check#

Checks the folder's settings without running — the same remarks as warnings when saving. ?folder= — the folder, ?mask= — a name or a mask (default: all).

200 Response
{
  "folder": "lut_now", "total": 125, "with_warnings": 2,
  "settings": [
    { "name": "T_BTW", "strategy": "zz", "warnings": [
      "zz.zz6_len = 8 is ignored: only when zz.type = ZZ6 (now: zz.type = \"ZZ2\")"
    ] }
  ]
}

settings contains only settings with remarks.

Errors
  • 404folder not found
POST/api/tester/run#

Mining of strategy parameters (the parameter_mining section of the tester config).

Request
{
  "settings": ["*"],
  "folder": "lut_now",
  "config": { ... },
  "overrides": [
    { "match": "T_*", "mul": { "basic.balance_percentage_long": 2 } },
    { "match": "C_MU", "set": { "basic.use_short": false } },
    { "match": "L_X", "exclude": true }
  ]
}
  • settings — settings names or masks ("*" — all settings in the folder). A name without a mask that is not in the folder is an error.
  • folder — settings folder, see Settings folder in a request.
  • config is optional: without it the saved tester config is used.
  • overrides — settings edits for this run only; settings files do not change. Rules apply in order. match — a name or a mask (default: all), tag — only settings with this tag. set replaces values, mul multiplies numbers (an integer field stays integer), cap_pct after them limits basic.balance_percentage_long and _short of these settings — a size multiplier with a per-pair ceiling: { "match": "T_*", "mul": { "basic.balance_percentage_long": 2, "basic.balance_percentage_short": 2 }, "cap_pct": 10 }. exclude: true removes settings from the run. Fields are dotted paths, as in the settings object. If a field is absent from all matching settings, it is an error (usually a typo); if it is absent from only some of them, those settings are listed in not_applied. The edits are written to the run's comment and end up in the report and the snapshot.
200 Response
{
  "success": true,
  "folder": "lut_now",
  "strategies": ["C_MU", "T_BTC"],
  "applied": [ { "name": "T_BTC", "changes": ["basic.balance_percentage_long: 10.0 → 20.0"], "not_applied": [] } ],
  "excluded": ["L_X"]
}

What can be mined. Any bot setting (settings[*].zz.depth, settings[T_*].basic.use_short, settings[NAME].…), any tester config field (config_tester.…), settings_program.… and the account. For numbers start/end/step is enough, for others (text, boolean) set values.

Pair screening. You do not need a settings file per pair: take one template setting and mine the symbol as a regular parameter. The list of pairs comes from tester_exchange_symbols:

"parameter_mining": [
  {
    "name": "settings[*].basic.symbol",
    "start": 1.0, "end": 1.0, "step": 1.0,
    "values": ["1000BONKUSDT", "1000PEPEUSDT", "AAVEUSDT", "ADAUSDT"]
  }
]

Together with other parameters (for example, settings[*].zz.depth) this gives a grid per pair. The symbol goes into a reports_history.csv column, so the “pair → grid median” summary is calculated from this file (read_tester_file with columns). tester_download_data downloads the data for all mined pairs. The number of combinations is the product of the value list lengths: limit it with monte_carlo.

The tester runs all value combinations from parameter_mining; if monte_carlo is set, a random sample of that size. Check the number of combinations in advance; to stop a running mining — POST /api/tester/stop with full: true. Progress is in run of the GET /api/tester/status response (runs_done of runs_total).

Results go to a report folder named after the tester config's name_comment (characters invalid in a file name are replaced with _). The run response returns it in report_folders; with walk_forward enabled there is also a second folder <name_comment>_WF_OOS with out-of-sample runs. To keep experiments apart, give each its own name_comment. In the folder:

  • reports_history.csv — one row per combination: metrics (TotalPnLPercent, MaxDrawdownPercent, ProfitFactor, WinRate, TradingVolume, …), Strategies (settings names of the run separated by |), RunTag and a column with the value of each mined parameter. With single_mode each row has one setting, combination blocks go by settings name;
  • an HTML report of each combination (if report.enable_html_report is on) and its JSON version (if report.enable_json_report is on); the combination's RunTag is in the file name;
  • the optimizer summary report report_optimizer_*.html;
  • run_snapshot_<name_comment>_<time>.json — exact settings and config of the run, see snapshot-run.

periods — mining on several periods: [["2026-02-01","2026-05-01"], ["2026-05-01","2026-08-01"]]. Each period is a separate full run into the folder <name_comment>_<start>_<end>; they go one after another through the queue. The response is { "success": true, "periods": [ { "start", "end", "report_folders", "queued", "queue_id", "position" } ], ... }. To compare periods — /api/tester/mining-robustness.

Run warnings. The run response contains warnings (null — no remarks):

  • a parameter_mining parameter that is in neither the settings nor the configs (a typo: it will not apply);
  • a mined field that the matching settings do not read (ignored by all N matching settings: all combinations give the same result) or that not all of them read; conditions on fields that are mined themselves are not checked;
  • a note about a field that matters only in edge cases (zz.zz6_risk_max);
  • a field from overrides that the settings do not read.

Warnings do not cancel the run.

Single runs and batches also write these warnings to the run results (warnings of wizard-runs/{run_id}), and a batch also to the common warnings with the runs[i] <name> prefix. A snapshot run is checked the same way.

Portfolio mining. Without single_mode all settings go together in each combination. A parameter named settings[NAME].path changes one setting, and settings[T_*].path changes all settings by a name mask; the rest of the set stays as is. Compare rows by the portfolio MaxDrawdownPercent and UlcerIndex (enable reports_history.ulcer_index).

sides — trading sides for this run: long (long only), short (short only), split (each setting twice — <name> [long] and <name> [short]; with single_mode the sides are mined separately) or both (as in the settings).

?queue=1 — if the tester is busy with another full run or a data download, the run joins the queue instead of getting 409. The response then is { "success": true, "queued": true, "queue_id": "63a8e791", "position": 1, ... }; the run starts by itself, the queue is visible in queue of the GET /api/tester/status response, to cancel one that has not started — POST /api/tester/queue/{queue_id}/cancel.

Errors
  • 400settings not found, invalid config, overrides or sides
  • 404folder not found
  • 409the tester is already running or data is downloading (without ?queue=1)
POST/api/tester/stress#

Stress test: the same settings under worse execution conditions. The body, ?queue=1 and the responses are as in /api/tester/run, except periods (not supported). Results are in the folder <name_comment>_STRESS (report_folders of the response): scenario rows in reports_history.csv and the comparison with the base in stress_test_*.html.

POST/api/tester/queue/{queue_id}/cancel#

Removes a full run that has not started yet from the queue.

200 Response
{ "success": true }
Errors
  • 404the run has already started, been cancelled, or queue_id is invalid
POST/api/tester/wizard-run#

A single run of strategies. The body is as in /api/tester/run, including folder and overrides.

200 Response
{ "success": true, "run_id": "a1b2c3d4", "folder": "lut_now", "strategies": ["T4 BTC"], "applied": [], "excluded": [] }

To wait for the run and get its metrics and report paths — GET /api/tester/wizard-runs/{run_id}?wait=. The progress of all runs is in wizard_runs of the GET /api/tester/status response.

Errors
  • 400none of the settings found
  • 409data is downloading
POST/api/tester/wizard-batch#

A batch of single runs: several sets, each on several periods, through one queue with limited parallelism. This is how to compare variants and check a variant on several periods with one request.

Request
{
  "folder": "lut_now",
  "config": { ... },
  "periods": [["2026-02-01", "2026-05-01"], ["2026-05-01", "2026-08-01"]],
  "parallel": 2,
  "runs": [
    { "name": "base", "settings": ["*"] },
    { "name": "T x2", "settings": ["*"], "overrides": [ { "match": "T_*", "mul": { "basic.balance_percentage_long": 2 } } ] },
    { "name": "long only", "settings": ["*"], "sides": "long" }
  ]
}

Set fields — as in the /api/tester/run body (settings, folder, config, overrides, sides) plus name and its own periods. What the set lacks is taken from the batch. Each set goes on each period as a separate run; without periods — on the config's period. parallel — how many runs at once (default 2; each keeps its data in memory). An error in any set rejects the whole batch.

200 Response
the same as GET /api/tester/wizard-batches/{batch_id}, right after queueing.
Errors
  • 400empty runs, invalid periods or a set error (runs[1]: ...)
  • 404folder not found
  • 409data is downloading
GET/api/tester/wizard-batches/{batch_id}#

The batch and the results of each run. ?wait= — how many seconds (up to 240) to wait for the whole batch to finish.

A long batch (dozens of runs) does not fit in a response in full form (MCP cuts it at 200,000 characters and the JSON becomes invalid), so there is a compact form and pages. The same parameters apply to POST /api/tester/wizard-batch:

ParameterDescription
compact=1each run has only name, start, end, run_id, status, run_params, key and report paths (message only for failed ones). Without summary, stats, warnings, applied
offset, limita page of the runs list; limit=0 — counters only. The response has offset, returned and has_more (there is a next page: offset + returned). done, total, failed always cover the whole batch

If the full response is longer than 150,000 characters, it is replaced with the compact one automatically (compact: true in the response); offset/limit pages are preserved.

key is in every run, including the full response: the main metrics in one form, from the JSON report's summary, or if there is no report, from the final stats: pnl, pnl_percent, max_drawdown_percent, max_drawdown_worst_percent (only with a JSON report), trades, volume, fees, profit_factor (only with a JSON report). There is no need to call reports/json for each run to get them.

200 Response
{
  "batch_id": "31dd7d67", "status": "Completed", "done": 2, "total": 2, "failed": 0, "parallel": 2,
  "started_utc": "2026-10-27T21:31:27Z",
  "compact": false, "offset": 0, "returned": 2, "has_more": false,
  "runs": [
    {
      "name": "T x2", "start": "2026-02-01", "end": "2026-05-01", "run_id": "a99984a8", "status": "Completed",
      "message": "Completed: 12 strategies.",
      "key": { "pnl": 1240.5, "pnl_percent": 12.4, "max_drawdown_percent": 5.2, "max_drawdown_worst_percent": 8.1, "trades": 140, "volume": 520000.0, "fees": 310.2, "profit_factor": 1.6 },
      "summary": { "pnl_percent": 12.4, "max_drawdown_worst_percent": 8.1, "ulcer_index": 2.3, "profit_factor": 1.6, "trades": 140, "monthly": { "2026-02": 3.1 } },
      "stats": null,
      "report_path": "...html", "report_json_path": "...json", "snapshot_path": "...json",
      "warnings": [],
      "applied": [ { "name": "T_BTC", "changes": ["basic.balance_percentage_long: 10.0 → 20.0"], "not_applied": [] } ],
      "excluded": null
    }
  ]
}

summary — as the summary section of the JSON report; it is present if report.enable_json_report is on, otherwise stats is there instead. Batch run rows also go to reports_history.csv of the name_comment folder (columns StartDate, EndDate, Strategies). Batches are kept for an hour.

Errors
  • 404no such batch: invalid batch_id, more than an hour has passed, or the program restarted
POST/api/tester/stop#

Stops tester runs.

Request at least one field is required
{ "run_id": "a1b2c3d4", "batch_id": "5e6f7a8b", "full": true, "clear_queue": true }
  • run_id — a single run (wizard-run);
  • batch_id — all runs of the batch: those not started will not start;
  • full: true — the current full run: mining, stress test, walk-forward, snapshot;
  • clear_queue: true — also remove full runs that have not started from the queue.

A running run stops at the next candle. An interrupted run writes neither a report nor a reports_history row: a result for part of the period would be misleading. A full run keeps the rows of combinations already calculated; in parallel mode child processes exit. Statuses: a full run — Stopped in run of the /api/tester/status response, a single run — Cancelled in wizard-runs/{run_id}. This request does not stop a historical data download.

200 Response
{ "success": true, "cancelled_runs": ["a1b2c3d4"], "full_run_stopping": true, "cleared_queue": ["63a8e791"], "note": "Stopping: ..." }
Errors
  • 400no field passed
  • 404run or batch not found
GET/api/tester/wizard-runs/{run_id}#

A single run by run_id. ?wait= — how many seconds (up to 240) to wait for it to finish; without it the response is immediate.

200 Response
{
  "run_id": "a82c93c5",
  "strategies": ["T4 BTC", "T4 TSLA"],
  "status": "Completed",
  "started_utc": "2026-10-25T05:26:43Z",
  "finished_utc": "2026-10-25T05:26:50Z",
  "current_bar": 27360,
  "total_bars": 27360,
  "message": "Completed: 2 strategies. 1 warnings.",
  "report_path": "T4/T4_run_001_of_001_system_2_ee8c3c2e.html",
  "report_json_path": "T4/T4_run_001_of_001_system_2_ee8c3c2e.json",
  "snapshot_path": "T4/run_snapshot_T4_20261025_082643.json",
  "stats": { "InitialBalance": 10000.0, "FinalBalance": 9581.25, "TotalPnL": -418.75, "TotalPnLPercent": -4.19, "TotalTrades": 23, "WinRate": 13.04, "MaxDrawdown": 527.89, "MaxDrawdownPercent": 5.26, "TotalFees": 22.2 },
  "warnings": ["T4 TSLA: symbol TSLAUSDT: no data before 2025-09-14 00:00"]
}

status — Running, Completed or Failed; Running after wait means the wait time is over — request again. report_path and report_json_path — paths inside root=report, null if the report was not written. snapshot_path — the run's snapshot: the exact settings and config it ran with. warnings — skipped settings, missing data and the reason there is no report. If no settings started, the run ends with status: "Failed" and the reason in message. Finished runs are kept for an hour (until the program restarts).

Errors
  • 404no such run: invalid run_id, more than an hour has passed, or the program restarted
POST/api/tester/snapshot-run#

Repeats a run from a snapshot file. Each run saves a snapshot next to the report — run_snapshot_<name_comment>_<time>.json with the sections settings (settings of all strategies), tester_config and program_config. Tokens (api_token, telegram.bot_token) are not written to the snapshot. A snapshot run is full mode: one at a time, like mining. The file is selected by the ?root=&path= parameters, as in tester files.

The body is optional: you can send a changed snapshot — changes apply to this run only, the file does not change. The same in the interface: Tester, “Results (files)” tab, the “Run” button next to a snapshot opens it in the editor, and the edits go to this run only.

Request
POST /api/tester/snapshot-run?root=report&path=T4/run_snapshot_20261025.json

?queue=1 — join the queue if the tester is busy, as in /api/tester/run.

200 Response
{ "success": true }
Errors
  • 400the snapshot has no tester config or settings
  • 404file not found
  • 409the tester is already running or data is downloading
POST/api/tester/download-data#

Downloads historical data for all settings in the folder (?folder=). Progress and result are in download of the GET /api/tester/status response.

ParameterDescription
startPeriod start, for example 2026-05-01. Default: the tester config's StartDate minus WarmupDays
endPeriod end. Default: the current moment (UTC). EndDate of the saved config is not used, so you do not need to change the config to download fresh days
200 Response
{ "success": true }

When finished, download.message briefly says up to which day the data is on disk, and download.result has first_day, last_day (the last day with candles) and days for each pair:

"download": {
  "active": false, "status": "Completed", "message": "Data on disk ends at 2026-10-31..2026-10-31 (last day with candles)",
  "result": { "requested_start": null, "requested_end": null,
    "pairs": [ { "symbol": "BTCUSDT", "exchange": "bybit", "first_day": "2025-10-01", "last_day": "2026-10-31", "days": 396 } ] }
}

The source publishes a closed day's candles with a delay (usually after 06:00 UTC): until then last_day stays a day behind and the download must be repeated later. A pair without data gets first_day/last_day = null.

Errors
  • 400start or end is not a date, or start is not earlier than end
  • 409a download is already in progress or the tester is running
GET/api/tester/wizard-results#

Results of single runs, newest first. A new run of the same settings set replaces its previous record; the result of a specific run is in /api/tester/wizard-runs/{run_id}.

200 Response
[
  {
    "runId": "a82c93c5",
    "timestamp": "2026-10-25T05:26:50Z",
    "strategies": ["T4 BTC"],
    "stats": {
      "InitialBalance": 10000.0, "FinalBalance": 9581.25, "TotalPnL": -418.75, "TotalPnLPercent": -4.19,
      "TotalTrades": 23, "WinRate": 13.04, "MaxDrawdown": 527.89, "MaxDrawdownPercent": 5.26, "TotalFees": 22.2
    },
    "chartUrl": "/tester-report/T4/T4_run_001_of_001_BTCUSDT_1h_ee8c3c2e.html",
    "report_path": "T4/T4_run_001_of_001_BTCUSDT_1h_ee8c3c2e.html",
    "report_json_path": "T4/T4_run_001_of_001_BTCUSDT_1h_ee8c3c2e.json",
    "period": "2025-09-01 .. 2025-09-20",
    "elapsed": "00:00:07"
  }
]

chartUrl — a link to the HTML report for the browser. report_path — the same report as a path inside root=report, for tester files and /api/tester/reports/summary. report_json_path — its JSON version; the field is present only if it was written.

GET/api/tester/files#

Contents of a tester folder. ?root= — report (reports, default), data (historical data: tester/data or the folder from tester_data_dir in settings_program.json) or runs (sets for repeat runs); ?path= — a path inside it. ?contains= — only names with this text, ?offset= and ?limit= — part of the list. Folders come first by name, then files, newest first; total — how many items match in total.

200 Response
{
  "root": "report",
  "path": "T4",
  "total": 2,
  "offset": 0,
  "items": [
    { "name": "old", "is_dir": true, "size": 204800, "modified_utc": "2026-10-20T10:00:00Z", "snapshot": false, "editable": false },
    { "name": "run_snapshot_20261025.json", "is_dir": false, "size": 51200, "modified_utc": "2026-10-25T12:00:00Z", "snapshot": true, "editable": false }
  ]
}

snapshot: true — the file can be run through /api/tester/snapshot-run. editable: true — the file can be changed through /api/tester/files/save.

Errors
  • 404folder not found
GET/api/tester/files/download#

Downloads a file (?root=&path=). HTML reports open in the browser.

Errors
  • 404file not found
POST/api/tester/files/delete#

Deletes a file or folder (?root=&path=).

200 Response
{ "success": true }
Errors
  • 400invalid path
  • 404not found
  • 409the tester is running or data is downloading
GET/api/tester/files/content#

Content of a snapshot file or a JSON file from runs (?root=&path=).

Errors
  • 404file not found or it is not a snapshot
POST/api/tester/files/save#

Saves a JSON file in runs (?root=runs&path=): changes an existing one or creates a new one. The body is the new file content. If the body for an existing file has no settings section, it is taken from the current file. A new file must be a full snapshot: non-empty settings and tester_config.

This is how a run is prepared for the user: in the interface (Tester, “Results (files)” tab, runs folder) each such file has “Run” and “Edit” buttons. With use_runs in the tester config, a full run from the command line (--run-tester) runs all .json from runs.

200 Response
{ "success": true, "created": false }
Errors
  • 400empty body, invalid JSON, or a new file without settings / tester_config
  • 404the path is not in runs or not .json
GET/api/tester/reports/summary#

Summaries of HTML reports for comparing runs: metrics and the equity curve. ?r= — report path in report, the parameter can be repeated.

Request
GET /api/tester/reports/summary?r=T4/report.html&r=T4_STRESS/report.html
200 Response
[
  {
    "report": "T4/report.html",
    "created": "2026-10-25T12:00:00Z",
    "summary": {
      "name_comment": "T4",
      "start": "2025-01-01T00:00:00",
      "end": "2026-01-01T00:00:00",
      "symbols": ["BTCUSDT"],
      "metrics": { "TotalPnLPercent": 42.1, "MaxDrawdownPercent": 12.3, "ProfitFactor": 1.8, "WinRate": 55.0, "TotalTrades": 120 },
      "equity_t": [1735689600],
      "equity_v": [1000.0]
    }
  }
]

equity_t and equity_v — time (Unix, seconds) and balance of the equity curve points. summary: null — the report has no summary.

Errors
  • 400no r passed
  • 404report not found
GET/api/tester/reports/json#

Detailed results of one run in compact JSON. The same numbers as in the HTML report, but the file is dozens of times smaller: tens of kilobytes instead of megabytes.

The tester writes <report>.json next to the HTML report if report.enable_json_report is on in the tester config (off by default). JSON is written even with report.enable_html_report off: this way a large mining run goes faster and still keeps the details of each combination.

Parameters:

  • ?path= — report path in report: a .json file or its .html (for example, report_path from wizard-results);
  • ?sections= — only these comma-separated sections, for example ?sections=summary or ?sections=metrics,by_symbol;
  • ?trades_offset= and ?trades_limit= — part of the trades.

Sections:

SectionContent
summarya 1–2 KB summary: PnL, drawdowns (including unrealized and the worst of three, like MaxDrawdownPercent in reports_history.csv), Ulcer Index, profit factor, Sharpe, CAGR, trades, win rate, fees, average and maximum position, worst month, monthly returns. Calculated on read from run, metrics and monthly, so old reports have it too
runname_comment, comment, run_tag, run_params (values of mined parameters), period, exchanges, strategies and pairs. What the run appended to comment as text ([overrides: …], [sides: …]) is in separate fields, there is no need to parse comment with a regex: overrides (an array of rules as in the request, not truncated; in comment it is cut to 300 characters), sides, and where the run came from: batch_id and name (set name) for tester_run_batch and tester_run_single with periods, run_id for wizard runs. If there is no value, the field is null. Runs from the interface do not fill these fields, a snapshot replay takes them from the snapshot
metricsthe whole statistics table of the HTML report by blocks: PnL, balance, monthly returns, trades, fills, Buy & Hold, risk and exposure, Sharpe, Sortino, CAGR, Calmar, Ulcer Index…
by_symbolresults per pair: trades, win rate, profit factor, PnL, best and worst trade, drawdown, Buy & Hold, fees, volume, position size
monthlymonthly returns, %: { "2025-09": -4.15 }
tradestrades (from opening a position to a full close or reversal), sorted by close time — columns and rows
curvesbalance (equity) and open position (open_positions_total, _long, _short) curves, up to 500 points; t — Unix time in seconds, v — values
dailyvalues at the end of each calendar day of the period, without gaps: days, equity (Margin balance), position (open position, USDT); strategies — strategies (settings) with pairs, code, type (trend / counter-trend) and tags; pnl and position_by_strategy — one array per strategy in the same order: the cumulative contribution to balance since the report start, and the position. Written with report.json_daily (on by default). For large sets the section is big — /api/tester/reports/portfolio gives a summary of it
settingsfull strategy settings of the run

The fields v (format version), created_utc and html_report (HTML report name or null) are always returned.

Request
GET /api/tester/reports/json?path=T4/T4_run_001_of_001_BTCUSDT_1h_ee8c3c2e.html&sections=by_symbol,trades&trades_limit=2
200 Response
{
  "v": 1,
  "created_utc": "2026-10-25T05:45:34Z",
  "html_report": "T4_run_001_of_001_BTCUSDT_1h_ee8c3c2e.html",
  "by_symbol": [
    { "symbol": "BTCUSDT", "strategy": "zigzag", "trades": 23, "win_rate": 13.04, "profit_factor": 0.395, "pnl": -390.14,
      "best_trade": 160.02, "worst_trade": -57.7, "max_drawdown_incl_unrealized": -483.2, "buy_hold_percent": 6.81,
      "fee": 22.2, "volume": 221995.16, "position_avg_percent": 49.31, "position_max_percent": 50.72 }
  ],
  "trades": {
    "count": 23,
    "columns": ["symbol", "side", "open", "close", "holding_hours", "fills", "net_pnl", "mae", "mfe"],
    "rows": [
      ["BTCUSDT", "short", "2025-09-01 00:04", "2025-09-01 07:54", 7.83, 2, -57.6968, -57.6968, 38.8311],
      ["BTCUSDT", "long", "2025-09-01 07:54", "2025-09-01 20:45", 12.85, 2, -46.2855, -50.7967, 26.7293]
    ],
    "offset": 0
  }
}

In trades: count — the number of all trades, net_pnl — trade result net of fees, mae and mfe — the worst and best intermediate trade result (USDT), time is UTC.

Errors
  • 400the file is not a JSON report
  • 404JSON report not found
GET/api/tester/reports/portfolio#

Portfolio analytics of a run based on the daily section of its JSON report. Strategies are collected into groups, and everything below is calculated by group.

Parameters:

  • ?path= — a JSON report (or its .html) in report;
  • ?group_by= — type (default: T — trend, CT — counter-trend, by strategy code), tag (the first settings tag) or strategy (each setting is its own group);
  • ?groups={"T":"T_*","CT":"CT_*"} — groups by name masks instead of group_by; settings without a match go to other;
  • ?worst_days= — how many worst days of each group to take (default 10);
  • ?curves=0, ?leave_one_out=0 — do not calculate these sections.
200 Response abridged
{
  "start": "2026-08-01", "end": "2026-09-30", "days": 61, "group_by": "type",
  "groups": [ { "group": "T", "strategies": 1, "names": ["T_BTW"], "pnl": -7294.0, "pnl_percent": -72.94, "worst_day": -4838.3, "max_drawdown_usdt": 15259.83, "position_max": 12752.08 } ],
  "correlation": { "CT": { "CT": 1.0, "T": -0.28 }, "T": { "CT": -0.28, "T": 1.0 } },
  "worst_week": { "start": "2026-08-08", "end": "2026-08-14", "pnl": -7200.62, "pnl_percent": -40.19, "by_group": { "CT": 279.98, "T": -7480.6 } },
  "worst_days": { "T": { "days": 5, "pnl": -12902.51, "portfolio_pnl": -12754.28, "others": { "CT": { "pnl": 148.22, "offset_percent": 1.1 } } } },
  "leave_one_out": {
    "max_drawdown_percent": 83.43, "ulcer": 59.776,
    "groups": { "T": { "pnl": -7294.0, "max_drawdown_percent_without": 1.54, "drawdown_added": 81.89, "ulcer_without": 0.686, "ulcer_added": 59.09 } },
    "strategies": [ { "name": "T_BTW", "group": "T", "pnl": -7294.0, "max_drawdown_percent_without": 1.54, "drawdown_added": 81.89, "ulcer_without": 0.686, "ulcer_added": 59.09 } ]
  },
  "curves": { "days": ["2026-08-01"], "equity": [10754.72], "position": [0], "pnl_by_group": { "T": [0] }, "position_by_group": { "T": [0] } }
}
  • correlation — correlation of the groups' daily PnL;
  • worst_week — the worst 7 days in a row for the portfolio and each group's PnL in them;
  • worst_days — for each group its worst days: its PnL, portfolio PnL and the PnL of other groups on the same days; offset_percent — what share of the loss another group won back (below zero — it added to the loss);
  • leave_one_out — maximum drawdown and Ulcer of the portfolio without each strategy and each group: drawdown_added above zero — the strategy adds drawdown, below zero — it reduces it (hedges). Replaces a separate run of the set without each strategy;
  • curves — daily curves without gaps: balance, position, cumulative PnL and position of each group.

All values are based on end-of-day figures: drawdowns here are smaller than the intraday ones from metrics, and leave_one_out is approximate — without the strategies affecting each other through margin and position limits.

Errors
  • 400the report has no daily section (report.enable_json_report and report.json_daily are required; old reports do not have it) or invalid group_by / groups
  • 404report not found
GET/api/tester/mining-robustness#

Mining robustness across several periods: the same parameter_mining grid run on each period into its own folder (periods in /api/tester/run).

Parameters:

  • ?folders=a,b — folders in report, one period in each;
  • ?metric= — a reports_history.csv column, default TotalPnLPercent; for drawdown, risk and UlcerIndex columns lower is better;
  • ?base={"settings[*].zz.depth": 12} — the starting point for comparison;
  • ?top= — how many combinations to show per setting (default 10).

Combinations are matched by parameter values and the Strategies column (with single_mode each setting has its own ranking). A plateau is calculated on each period — the mean metric over a combination and its grid neighbours (as in the robustness section of the optimizer report). Ranking is by the mean plateau of the periods, then by the worst period.

200 Response abridged
{
  "metric": "TotalPnLPercent", "lower_is_better": false,
  "periods": ["T4_2026-08-01_2026-08-31", "T4_2026-09-01_2026-09-30"],
  "parameters": ["settings[*].basic.balance_percentage_long"],
  "combinations_common": 5,
  "by_strategies": [
    {
      "strategies": "CT_VELVET|T_BTW",
      "top": [ { "rank": 1, "parameters": { "settings[*].basic.balance_percentage_long": "50" }, "mean_plateau": -42.16, "worst_period": -56.75, "mean": -41.74,
                 "by_period": [ { "period": "T4_2026-08-01_2026-08-31", "metric": -26.72, "plateau": -28.02, "run_tag": "run_005_of_005" } ], "beats_base": true } ],
      "best_by_period": [ { "period": "T4_2026-09-01_2026-09-30", "parameters": { "settings[*].basic.balance_percentage_long": "10" }, "metric": -52.97, "rank_across_periods": 5, "worst_period": -52.97 } ],
      "base": { "rank": 1, "...": "..." },
      "beats_base_count": 1
    }
  ]
}

best_by_period — the best point of each period and its place in the overall ranking: a low place means the point is fitted to its period. beats_base — the combination is no worse than the base on any period.

Errors
  • 400fewer than two folders, no metric column, or too few combinations for neighbours
  • 404folder not found
POST/api/tester/reports/telegram#

Sends a tester report file to Telegram — to the tester notification chat (telegram.id_chat of the program config, where mining results also arrive). Only a file from report can be sent (.html, .json, .csv, .png, .txt, up to 50 MB); there is no access to trading or strategy chats. The caption is Tester report: <path> and caption.

Request
{ "path": "T4/T4_BTCUSDT_20261025.html", "caption": "Best variant by drawdown" }
200 Response
{ "success": true, "path": "T4/T4_BTCUSDT_20261025.html" }

No more than one file every 10 seconds.

Errors
  • 400the file is not a report or is larger than 50 MB, telegram.id_chat is not set
  • 404file not found
  • 429too often
  • 502Telegram rejected the file (the reason is in the program log)
GET/api/tester/publish/files#

Report folder files that can be published to the site. ?path= — a folder in report.

200 Response
[ { "name": "reports_history.csv", "size": 10240, "kind": "history" } ]

kind — history (run history), snapshot (run snapshot) or summary (summary report).

Errors
  • 404folder not found
POST/api/tester/publish#

Sends the selected report folder files to the site. The publication appears after moderation.

Request
{ "path": "T4", "files": ["reports_history.csv"], "contact": "@telegram", "comment": "...", "agree": true }

agree: true — consent to the publication terms, required.

200 Response
{ "success": true, "sent": ["reports_history.csv"] }
Errors
  • 400no consent or no files selected
  • 404folder not found
  • 502the site rejected the publication

Response codes#

CodeMeaning
200Success
400Bad request: empty body, invalid JSON, required fields are empty
401No access: invalid api_token; the cookie is missing, expired or replaced by a new login; a request without a token not from the same machine
403A request from a page of another site (Origin does not match the bot's address)
404Not found
409Conflict: the tester or a data download is already running, the name is taken, the required running bot is missing
429Too often (sending a tester report to Telegram)
500Internal error or exchange error
502An external service rejected the request
503The program is still starting