Skip to main content
Glama
roman-zaglauer

OctoBot MCP Server

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
OCTOBOT_BASE_URLYesBase URL of the OctoBot instance (e.g., http://192.168.178.100:5001). Required; the server fails fast if unset or invalid.
OCTOBOT_MCP_LOG_FILENoLog file path (parent directory created if missing).~/.octobot-mcp/server.log
OCTOBOT_MCP_LOG_LEVELNoRoot logger level for the file handler.INFO
OCTOBOT_MCP_LOG_MAX_BYTESNoRotation size threshold.5000000
OCTOBOT_MCP_LOG_BACKUP_COUNTNoNumber of rotated backups kept.3

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
get_ordersA

List every order OctoBot currently knows about (open and historical).

Maps to GET /api/orders. Read-only, not confirm-gated. Returns OctoBot's own JSON body unchanged (NFR-8).

get_positionsA

List every trading position OctoBot currently knows about.

Maps to GET /api/positions. Read-only, not confirm-gated. Returns OctoBot's own JSON body unchanged (NFR-8).

get_tradesA

List OctoBot's trade history.

Maps to GET /api/trades. Read-only, not confirm-gated. Returns OctoBot's own JSON body unchanged (NFR-8).

get_pnl_historyA

Fetch realized PnL history, optionally filtered.

Maps to GET /api/pnl_history -- query parameters confirmed against OctoBot source (api/trading.py::pnl_history(), master branch, [V] this session): exchange, symbol, quote, since, and scale are all optional filters OctoBot itself reads from the query string. Omit any you don't want to filter by. Read-only, not confirm-gated. Returns OctoBot's own JSON body unchanged (NFR-8).

get_historical_portfolio_valueA

Fetch historical portfolio value samples, optionally filtered.

Maps to GET /api/historical_portfolio_value -- query parameters confirmed against OctoBot source (api/trading.py::historical_portfolio_value(), master branch, [V] this session): currency (OctoBot defaults this to "USDT" server-side if omitted), time_frame, from_timestamp, to_timestamp, and exchange are all optional query-string filters. Read-only, not confirm-gated. Returns OctoBot's own JSON body unchanged (NFR-8).

clear_orders_historyA

Permanently clear OctoBot's stored orders history. Confirm-gated (ADR-0003).

If confirm is not exactly true, no OctoBot call is made at all -- this returns require_confirmation's structured refusal (a normal return, not an error) instead.

Once confirmed, maps to POST /api/clear_orders_history ([V], re-confirmed against source this session -- see this module's docstring). Returns OctoBot's own JSON body unchanged (NFR-8): {"title": "Cleared orders history"}.

clear_trades_historyA

Permanently clear OctoBot's stored trades history. Confirm-gated (ADR-0003).

If confirm is not exactly true, no OctoBot call is made at all -- this returns require_confirmation's structured refusal (a normal return, not an error) instead.

Once confirmed, maps to POST /api/clear_trades_history ([V], re-confirmed against source this session -- see this module's docstring). Returns OctoBot's own JSON body unchanged (NFR-8): {"title": "Cleared trades history"}.

clear_portfolio_historyA

Permanently clear OctoBot's stored historical portfolio values. Confirm-gated (ADR-0003).

If confirm is not exactly true, no OctoBot call is made at all -- this returns require_confirmation's structured refusal (a normal return, not an error) instead.

Once confirmed, maps to POST /api/clear_portfolio_history ([V], re-confirmed against source this session -- see this module's docstring). Returns OctoBot's own JSON body unchanged (NFR-8): {"title": "Cleared portfolio history"}.

clear_transactions_historyA

Permanently clear OctoBot's stored transactions history. Confirm-gated (ADR-0003).

If confirm is not exactly true, no OctoBot call is made at all -- this returns require_confirmation's structured refusal (a normal return, not an error) instead.

Once confirmed, maps to POST /api/clear_transactions_history ([V], re-confirmed against source AND reproduced live this session against the OctoBot 2.1.1 test instance -- see this module's docstring). Returns OctoBot's own JSON body unchanged (NFR-8): {"title": "Cleared transactions history"}.

cancel_orderA

Cancel one order by id.

Maps to POST /api/orders?action=cancel_order -- action is a query parameter, not a JSON body field (confirmed against source, the same pattern octobot_mcp.tools.tentacles.update_tentacle_config already documents for its own route). The JSON body is the raw order_id value itself, NOT wrapped in an object: confirmed against source (Services/Interfaces/web_interface/api/trading.py::orders() does interfaces_util.cancel_orders([flask.request.get_json()]), and cancel_orders(order_ids) -- packages/services/octobot_services/ interfaces/util/order.py -- iterates its argument directly as a list of order ids). Use the id field from get_orders()'s own response items for this value.

Success: OctoBot returns HTTP 200 with the literal JSON string "Order cancelled", returned unchanged (NFR-8). Failure -- the order wasn't found on any connected exchange, including the case where every exchange manager tried raised an internal cancel error OctoBot itself silently swallows rather than propagating (see this module's docstring) -- raises OrderCancelRejectedError with OctoBot's own extracted error text (always, in practice, the literal "Impossible to cancel order: order not found.").

Not confirm-gated: the user was explicitly asked about the spec's own footnote flagging this tool as "revisitable" given its irreversible effect, and chose to keep it ungated (milestone 13.6), per ADR-0003's original reasoning that this is routine, expected trading-bot activity. No confirm parameter exists on this tool at all.

close_positionA

Close one open position, identified by symbol and side.

Maps to POST /api/positions?action=close_position -- action is a query parameter, not a JSON body field (same pattern as cancel_order above). A genuine spec correction: the spec names this tool's parameter position_id, but OctoBot positions have no id concept at all -- confirmed against source (Services/Interfaces/web_interface/ models/trading.py::_dump_position, the exact function behind get_positions()'s response, builds each position dict with symbol/side/contract/... keys but no id key, unlike _dump_order's sibling function which does include "id": order.order_id). The real route calls interfaces_util.close_positions( [{"symbol": ..., "side": ...}]) -- packages/services/ octobot_services/interfaces/util/position.py -- which reads exactly positions_desc["symbol"]/positions_desc["side"]. side is submitted as OctoBot's own PositionSide enum's literal value ("long"/ "short"/"both"/"unknown", confirmed against octobot_trading.enums.PositionSide) -- the exact same string get_positions()'s own side field already uses, so a value read from that response can be passed straight through as both symbol and side here.

Success: OctoBot returns HTTP 200 with the literal JSON string "Position closed", returned unchanged (NFR-8). Failure -- no matching open position on any connected exchange -- raises PositionCloseRejectedError with OctoBot's own extracted error text (always, in practice, the literal "Impossible to close position: position already closed.").

Not confirm-gated: the user was explicitly asked about the spec's own footnote flagging this tool as "revisitable" given its irreversible effect, and chose to keep it ungated (milestone 13.6), per ADR-0003's original reasoning that this is routine, expected trading-bot activity. No confirm parameter exists on this tool at all.

refresh_portfolioA

Trigger an immediate refresh of every enabled real (non-simulated) trader's portfolio.

Maps to POST /api/refresh_portfolio -- confirmed against source as a bare POST with no request body at all (the route never calls flask.request.get_json()).

Success: OctoBot returns HTTP 200 with the literal JSON string "Portfolio(s) refreshed", returned unchanged (NFR-8). Failure -- zero enabled real traders across every connected exchange (confirmed against source, octobot_services.interfaces.util.portfolio:: async_trigger_portfolios_refresh raises RuntimeError("no real trader to update.") in exactly this case) -- raises PortfolioRefreshFailedError with OctoBot's own extracted error text (always, in practice, the literal "No portfolio to refresh"). This is an entirely expected outcome on any OctoBot instance running only simulated trading, not a bug.

Not confirm-gated (ADR-0003): triggers a routine, non-destructive portfolio data refresh, not a trade action. No confirm parameter exists on this tool at all.

get_currency_listA

List OctoBot's globally configured trading currencies/symbols.

Maps to GET /api/currency_list. exchange is accepted for forward-compatibility with this tool's spec entry, but the currently deployed OctoBot route (api/trading.py::currency_list(), master branch, confirmed by reading its body this session) takes no request arguments at all and always returns the same global list regardless of this value -- it is a no-op filter on today's OctoBot. Read-only, not confirm-gated. Returns OctoBot's own JSON body unchanged (NFR-8).

get_all_currenciesA

List every currency OctoBot knows about for one exchange.

Maps to GET /api/get_all_currencies/<exchange> ([V], confirmed against OctoBot source api/config.py). Read-only, not confirm-gated. Returns OctoBot's own JSON body unchanged (NFR-8).

get_all_symbolsA

List every trading symbol/pair OctoBot knows about for one exchange.

Maps to GET /api/get_all_symbols/<exchange> ([V], confirmed against OctoBot source api/config.py). Read-only, not confirm-gated. Returns OctoBot's own JSON body unchanged (NFR-8).

check_accounts_compatibleA

Check whether each named exchange's already-configured account is usable.

Maps to POST /api/are_compatible_accounts. OctoBot's real request body for this route is more general than this tool's input -- it expects a dict keyed by exchange, each value an object with exchange/apiKey/apiSecret/apiPassword/sandboxed fields meant for OctoBot's own "test these credentials" UI form (confirmed against OctoBot source models/configuration.py::are_compatible_accounts() and octobot_commons.constants, [V] this session -- this is a correction of this tool's spec entry, which only names exchange_names; see the implementation report for detail).

This tool deliberately narrows that to just exchange_names and always submits OctoBot's own masked-placeholder value ("******") for the credential fields, so it checks compatibility using whatever credentials are already stored on the OctoBot instance for each named exchange, never a credential supplied through this call. Confirmed from source: OctoBot only treats a non-placeholder value as a real credential update (_is_real_exchange_value) and otherwise falls back to the exchange's already-configured encrypted value, so a placeholder-only request checks the existing configuration without ever transmitting or requiring a real credential. No credential value passes through this tool's input or output. Read-only in effect, not confirm-gated. Returns OctoBot's own JSON body unchanged (NFR-8).

CAVEAT: sandboxed is currently hardcoded to False for every exchange checked, regardless of how that exchange is actually configured on the instance. It is unconfirmed whether OctoBot's credential-fallback path uses this value to pick which environment (testnet vs. production) to validate stored credentials against; if it does, a genuinely sandboxed exchange may be checked against the wrong environment. Treat this tool's result with caution for any exchange you know to be configured in sandbox/testnet mode until this is confirmed against source.

get_exchange_detailsA

Fetch ONE exchange's name and internal id.

Maps to GET /api/first_exchange_details ([V]). IMPORTANT: this endpoint only ever returns a single exchange -- the one matching exchange_name if given, or OctoBot's own notion of "the first exchange" otherwise -- confirmed against OctoBot source (api/exchanges.py::first_exchange_details()). It does NOT enumerate every exchange configured on the instance; do not call this tool expecting a full exchange list, and do not assume its result is the only configured exchange. Read-only, not confirm-gated. Returns OctoBot's own JSON body unchanged (NFR-8).

update_exchange_credentialsA

Write an exchange's API credentials to OctoBot's global config. Confirm-gated (ADR-0003).

If confirm is not exactly true, no OctoBot call is made at all -- this returns require_confirmation's structured refusal (a normal return, not an error) instead. Reason given: "writes exchange API credentials; verify the exchange name and that you intend to grant this key trading access" (per spec).

Once confirmed, maps to POST /config with JSON body {"global_config": {"exchanges_<exchange>_api-key": api_key, "exchanges_<exchange>_api-secret": api_secret[, "exchanges_<exchange>_ api-password": api_password]}} -- see this module's docstring ("Milestone 12") for the full, source-verified derivation of this exact shape (open question #2, now resolved) and its known limitation.

Refuses locally, before any OctoBot call, if exchange contains an underscore -- confirmed against source (octobot_commons.configuration.config_operations.parse_and_update) that OctoBot's own config-path decoder would otherwise split the generated "exchanges_<exchange>_api-key" key on every literal "_" positionally, silently writing the credential into the wrong nested config location instead of the intended exchange's. Raises ExchangeCredentialsUpdateRejectedError for this case.

INV-4/NFR-6, this tool's one consumer of redact_secrets: OctoBot's own success response for this route (global_updated_config, confirmed against source) echoes the ENTIRE submitted global_config dict back verbatim -- including the raw api_key/api_secret/api_password just sent. This tool therefore never reads OR forwards any part of OctoBot's response body on success: a 2xx status alone is trusted (this route's status code IS a faithful (success, err_message) signal, confirmed against source models/configuration.py::update_global_config, unlike several profiles.py routes that needed a post-hoc re-check), and the tool's own output below is built from only what it already knows locally. On failure, OctoBot's raw error text is passed through redact_secrets with the literal api_key/api_secret/api_password values from THIS call before being included in the raised exception's message -- the single boundary INV-4 requires, not scattered ad-hoc scrubbing.

Output on success: {"exchange": str, "updated": true, "api_key_last4": str} -- never the full api_key/api_secret, per NFR-6.

list_profilesA

List every profile OctoBot knows about, including which one is selected.

Tier-B (ADR-0006): scrapes /profiles_selector -- see this module's docstring for the verified DOM shape and fail-loud assumptions. Idempotent, read-only, not confirm-gated.

warning is non-null only when one or more profiles have neither a real nor a simulated trader enabled in their config -- a legitimate OctoBot state (confirmed against source) this adapter reports as profile_type: "UNKNOWN" rather than failing loud, since it is not a broken-scrape condition.

get_profileA

Fetch one profile's details, defaulting to the currently selected profile.

Calls the same scrape list_profiles uses and filters the result (spec: "same scrape, filtered") rather than scraping again. Raises ProfileNotFoundError if profile_id is given but matches no scraped profile -- distinct from ProfileScrapeContractBrokenError, which means the scrape itself is broken, not that this id doesn't exist.

create_profileA

Create a new profile, one of 4 ways, discriminated by mode.

Not confirm-gated (ADR-0003): creates new state, never destroys anything or touches credentials/live trading. Every mode's exact request shape was verified against OctoBot source (controllers/configuration.py's profiles_management route) and, where practical, against the live test instance -- see each _create_profile_* helper's docstring for the per-mode detail and the two spec corrections found along the way (import_file never returns JSON; import_url is presently broken upstream). Only the arguments relevant to the chosen mode need to be supplied; the rest are ignored:

  • mode="duplicate": requires source_profile_id.

  • mode="import_file": requires file_base64 (the file's raw bytes, base64-encoded) and filename.

  • mode="import_url": requires url.

  • mode="import_cloud_strategy": requires strategy_id and name; description is optional (defaults to "").

Output: {"profile_id": str, "name": str, "message": str}.

Raises ProfileImportFailedError for any failure in any mode (including a missing required argument for the chosen mode, or an unrecognized mode) -- OctoBot's own error text is included where one exists.

update_profileA

Update an existing profile's metadata (never the active/selected profile's name).

Maps to POST /profiles_management/update with JSON body {"id": profile_id, **patch} (confirmed against source models/profiles.py::update_profile(), which reads exactly this shape).

Spec corrections, both verified against source and reproduced live:

  • patch["risk"]/patch["complexity"] must be an int (or int-parseable string) matching OctoBot's enum ordinals -- not the display strings list_profiles returns ("Low", "Difficult", ...). Source (octobot_commons.enums): ProfileRisk is LOW=1, MODERATE=2, HIGH=3; ProfileComplexity is EASY=1, MEDIUM=2, DIFFICULT=3. Sending a display string (confirmed live) raises an uncaught ValueError inside OctoBot, surfaced by this tool as a ProfileUpdateRejectedError wrapping "invalid literal for int() with base 10: 'Moderate'", HTTP 500 -- not the clean UPDATE_REJECTED 400 path.

  • patch["config"] is accepted by this tool's input schema for forward-compatibility with the documented spec shape, but is currently silently ignored by OctoBot's deployed route: the controller only ever calls models.update_profile(id, data) -- never passing the third json_profile_content parameter that is the only thing update_profile() actually assigns to profile.config. There is no way for this tool to make config take effect against the current OctoBot version.

  • Renaming the currently-selected profile is rejected by OctoBot itself ("Can't rename the active profile", confirmed live), surfaced here as ProfileUpdateRejectedError with that exact message.

Output: {"profile_id": str, "updated_fields": object} (updated_fields echoes the patch this tool sent -- OctoBot's own success response is the same data echoed back, so there is nothing more to report).

Not confirm-gated (ADR-0003): does not destroy anything or touch credentials/live trading.

select_profileA

Select a profile as OctoBot's current/active profile.

Maps to GET /profile?select=<profile_id> (spec [V]).

Spec correction, verified against source (controllers/configuration.py::profile()) and reproduced live: this route always responds HTTP 200 with a full HTML page, whether or not the selection actually happened -- an unknown profile_id makes models.select_profile() raise NoProfileError, which the route catches internally, flashes a warning ("The requested profile no longer exists.") into an HTML-only, session-cookie-based flash message, and re-renders the PREVIOUSLY-selected profile's page, still with a 200 status. There is no way to distinguish success from this silent-failure case from the HTTP response alone. This tool therefore always re-fetches the currently-selected profile afterward (the same scrape list_profiles/get_profile use) and raises ProfileSelectionFailedError if it doesn't match profile_id, rather than trusting the 200 status.

Output: {"profile_id": str, "selected": true}. Idempotent (selecting the already-selected profile is a verified no-op on OctoBot's side) and not confirm-gated (ADR-0003).

export_profileA

Export a profile as a zip archive, base64-encoded.

Maps to GET /profiles_management/export?profile_id=<profile_id> (spec [V]; profile_id confirmed as the exact query-param name against source controllers/configuration.py, action "export" -- not id). This route returns the zip's raw bytes directly (flask.send_file), not JSON, so this tool bypasses octobot_mcp.client.request_json (same reason as _fetch_profiles_selector_html) and base64-encodes the raw response body instead.

filename is read from the response's Content-Disposition header (confirmed live: attachment; filename=<profile name>_<YYYYMMDD-HHMMSS>.zip, quoted only when the profile name isn't already a single HTTP token -- e.g. it contains a space -- per RFC 6266/2183; both forms confirmed live, see octobot_mcp.tools._shared._parse_content_disposition_filename, moved there in milestone 13 so octobot_mcp.tools.tentacles.export_logs shares this same implementation) rather than reconstructed locally, so it always matches exactly what OctoBot named the file. Raises OctoBotMalformedResponseError if that header is missing or unparseable -- never returns a fabricated filename.

Output: {"profile_id": str, "filename": str, "content_base64": str}. Read-only, not confirm-gated (ADR-0003). An unknown profile_id raises an uncaught NoProfileError inside OctoBot (HTTP 500, confirmed live with the Content-Type: application/json request header this call always sends for a readable error message).

delete_profileA

Permanently delete a profile. Confirm-gated (ADR-0003): irreversible.

If confirm is not exactly true, no OctoBot call is made at all -- this returns require_confirmation's structured refusal (a normal return, not an error) instead.

Once confirmed, maps to POST /profiles_management/remove with JSON body {"id": profile_id} (confirmed against source controllers/configuration.py's "remove" action, which reads exactly flask.request.get_json()["id"] -- the spec's documented body shape is correct here, unlike update_profile's in milestone 5).

Two failure modes confirmed against source (models/profiles.py::remove_profile), both surfaced as ProfileDeleteRejectedError:

  • profile_id is the currently-selected/active profile: OctoBot itself rejects this ("Can't remove the active profile"), returned as HTTP 400 with that exact string as the JSON body.

  • profile_id names a profile whose removal a ProfileRemovalError blocks (e.g. an in-use profile): also HTTP 400, wrapping that error's text. An unrecognized profile_id isn't checked by remove_profile before this tool ever reaches OctoBot: it's rejected locally as ProfileNotFoundError (via the same pre-fetched scrape used to look up name, below) rather than let OctoBot's own uncaught KeyError produce an HTTP 500.

Extra safety check, not strictly required by OctoBot's own response but added anyway: OctoBot's 200 success body for this action is the same uninformative literal string "Profile created" copied from the unrelated duplicate action (confirmed against source) -- not "Profile deleted" or anything that actually confirms removal. Given that copy-paste bug, this tool re-checks via the same scrape list_profiles/get_profile use that profile_id is genuinely gone afterward, raising ProfileDeleteRejectedError (not trusting the 200) if it is still present.

The profile's name is captured from a scrape taken before deletion (per this tool's spec entry), since after a real deletion it can no longer be found by scraping.

Output on success: {"profile_id": str, "name": str, "deleted": true}.

convert_profile_to_liveA

Relabel a profile as OctoBot's internal ProfileType.LIVE and select it. Confirm-gated (ADR-0003).

This tool does NOT enable real-money trading -- confirmed against OctoBot source, not just suspected (resolves requirements doc open question #5). Whether OctoBot actually places real orders is governed entirely by a separate config flag, config.trader.enabled, checked by OctoBot's own is_real_trading(profile) (semantically: returns trading_util.is_trader_enabled(profile.config), itself config[CONFIG_TRADER][CONFIG_ENABLED_OPTION] -- both confirmed against source, not a verbatim one-line quote of the actual multi-statement function body). models.convert_to_live_profile and models.select_profile (the two functions this tool's route calls -- see "Spec correction #1" below) both leave config.trader/config.trader-simulator completely untouched; only profile.profile_type (a label) changes. The one HTTP-visible route this project found that DOES set config.trader.enabled (_save_distribution_user_config, reached via save_prediction_market_configuration in OctoBot's web-interface source) is registered only when the running OctoBot instance's distribution is OctoBotDistribution.PREDICTION_MARKET -- an elif branch mutually exclusive with the DEFAULT distribution this project assumes throughout (confirmed against controllers/__init__.py::register()); it has nothing to do with OctoBot's onboarding wizard (a separate, always-registered controllers/welcome.py). This route is therefore unreachable on the ordinary/default OctoBot instance this server targets, not because of this project's own "onboarding" out-of-scope exclusion (a previous version of this docstring cited that exclusion; it was the wrong reason, even though the practical conclusion below still holds). As of this writing, this server has NO tool that can actually toggle real-vs-simulated trading on a default OctoBot instance. If you need that, it currently requires editing config.trader/config.trader-simulator in the profile's own saved config file directly (outside this server) and re-importing/re-selecting the profile -- there is no safer, HTTP-API-driven path this server can offer today.

If confirm is not exactly true, no OctoBot call is made at all -- this returns require_confirmation's structured refusal (a normal return, not an error) instead.

Once confirmed, maps to GET /profiles_management/use_as_live?profile_id=<id> (confirmed against source controllers/configuration.py's "use_as_live" action).

Spec correction #1, verified against source and reproduced live: this route's one non-raising code path calls models.convert_to_live_profile(profile_id) then models.select_profile(profile_id), flashes an HTML-only session message, and returns flask.redirect(flask.url_for("profile")) -- an HTTP 302 to /profile with no informative body at all, whether or not the conversion actually took effect. This is the same uninformative-GET-response category milestone 5 already found for duplicate (also a bare success marker) and select_profile (a 200 that can silently mean "nothing changed"). Following the same pattern select_profile established, this tool never trusts the response status alone: it re-fetches via the same scrape list_profiles/ get_profile use afterward and raises ProfileConversionFailedError if profile_id isn't selected there (see spec correction #2 for why only is_selected, not profile_type, is re-checked this way).

Spec correction #2 -- a genuine terminology collision in OctoBot's own data model, found live and confirmed against source, NOT just a misreading of this milestone's own instructions: is_selected/ profile_type were expected to both be independently re-verifiable post-hoc via the same scrape list_profiles uses (mirroring select_profile's pattern). is_selected is: the profile-overview-selected CSS class this scrape already parses reliably reflects models.select_profile(profile_id)'s effect. profile_type, however, is NOT reverified here, because the scrape's profile_type field (list_profiles/get_profile's "LIVE"/ "SIMULATOR"/"UNKNOWN") and the profile.profile_type attribute models.convert_to_live_profile actually sets are two unrelated OctoBot concepts that merely share a confusingly similar name:

  • The scrape's profile_type comes from the badge-info badge text, which is the return value of get_enabled_trader(profile) (confirmed against source flask_util/context_processor.py): "Real trading" iff trading_util.is_trader_enabled(profile.config), else "Simulated trading" iff is_trader_simulator_enabled, else no badge at all -- entirely about whether a real (vs. simulated) trader is enabled in that profile's OWN trading config.

  • models.convert_to_live_profile (confirmed against source models/profiles.py) only does profile.profile_type = commons_enums.ProfileType.LIVE; profile.validate_and_save_config(). octobot_commons.enums.ProfileType (confirmed against source) has exactly two members, LIVE = "live" and BACKTESTING = "backtesting" -- there is no SIMULATOR member at all, and this attribute is never rendered anywhere in /profiles_selector's HTML (confirmed by reading components/config/profiles.html and context_processor.py: neither references profile.profile_type).

Concretely (reproduced live against a freshly-duplicated, default SIMULATOR-trader profile): after a successful convert_profile_to_live call, the scrape's profile_type for that profile is still "SIMULATOR" -- convert_to_live_profile never touches the trader-enabled config the badge reflects. Gating this tool's success on profile_type == "LIVE" (as this milestone's own instructions originally called for) would therefore make it report failure on essentially every real invocation, which is worse than not checking it at all. There is no OctoBot HTTP-visible signal (Tier A or B) this server can use to independently confirm profile.profile_type flipped -- an acknowledged, documented gap (the same category as update_profile's undeliverable config field from milestone 5), not a silently-accepted assumption. This tool's "profile_type": "LIVE" in its own output below is therefore an echo of what was requested (and, per convert_to_live_profile's unconditional, non-branching implementation, reliably applied whenever this call doesn't raise -- see spec correction #3), not an independently re-scraped fact.

Spec correction #3, on why a non-5xx response is nonetheless a reasonably strong signal for the profile_type half specifically: unlike remove_profile's check-and-return-(result, err) pattern, convert_to_live_profile's body (confirmed against source) has no conditional branch that could skip the profile.profile_type = ProfileType.LIVE assignment -- it either runs to completion (assigns, then saves) or raises (propagating to the uncaught-500 path this tool already classifies as OctoBotServerError). A non-5xx response therefore does mean that assignment executed and was saved; the remaining genuine uncertainty this tool resolves by re-checking is only whether the immediately-following models.select_profile(profile_id) call (a separate function with its own historically-silent-failure mode for unknown ids, per select_profile's docstring) actually took effect -- which is exactly what the is_selected re-check above verifies.

An exception raised inside OctoBot's own handling (e.g. an unrecognized profile_id, which makes the underlying get_profile() raise) is not caught by this route at all, so it propagates to OctoBot's global error handler as an HTTP 500 -- classified here as OctoBotServerError, with the Content-Type: application/json request-header hint (same established technique as duplicate/select_profile/export_profile) so that handler's message is readable JSON instead of an HTML page.

Output on success: {"profile_id": str, "profile_type": "LIVE", "selected": true}.

get_job_statusA

Fetch one job's current status from the in-memory job store.

Maps to no OctoBot route at all -- purely an in-memory lookup (docs/specs/octobot-mcp-tool-spec.md's job inventory table lists this tool's "Maps to" as "in-memory job store only"). Read-only, not confirm-gated. Raises JobNotFoundError (surfaced as a ToolError by with_error_handling) for an unknown job_id, noting the server may have restarted (ADR-0002 risk table) -- an unknown id is expected in that case, not a bug.

list_jobsA

List jobs known to the in-memory job store, optionally filtered.

Maps to no OctoBot route -- purely an in-memory listing. state ("queued"|"running"|"completed"|"failed"|"cancelled") and kind ("backtest"|"compare_strategies") are both optional filters; omit either (or both) to not filter on it. Read-only, not confirm-gated. Each entry uses get_job_status's shape minus sub_jobs (spec's #### list_jobs(state?, kind?)).

cancel_jobA

Cancel a job, or report its existing terminal state if already finished.

Not confirm-gated (ADR-0003's explicit non-gated list): reversible, no data loss. Idempotent: cancelling an already-terminal job just returns its existing status, never an error.

This milestone implements only the state-machine transitions that don't require calling OctoBot (no HTTP/Socket.IO call happens here, or anywhere else in this module):

  • queued (waiting on backtest_execution_lock, never started): cancelled directly.

  • running: only sets cancel_requested = True on the record. Milestone 8's watcher loop is what will actually notice the flag, tell OctoBot to stop, and transition the record to cancelled once that's confirmed -- this tool deliberately does NOT fabricate an immediate state="cancelled" for a running job, since nothing here is actually stopping it.

Raises JobNotFoundError for an unknown job_id (same as get_job_status).

start_backtestA

Start a real OctoBot backtest run in the background and return immediately.

Not confirm-gated (ADR-0003): starts a simulation, never touches live trading or destroys data. Two mutually exclusive modes (all other parameters are mode-specific; unused ones for the chosen mode are ignored):

  • mode="data_files" -> OctoBot's start_backtesting action [V]. Requires files (a non-empty list of exact data file names -- there is currently no MCP tool exposing what's available; the agent must already know a valid name, e.g. from OctoBot's own /backtesting page). start_timestamp/end_timestamp are epoch milliseconds (confirmed against source: _start_backtesting divides by 1000 before use), matching this project's convention elsewhere. run_on_common_part_only defaults to True (OctoBot's own default when omitted).

  • mode="current_bot_data" -> OctoBot's start_backtesting_with_current_bot_data action [V]. Every field is technically optional at the HTTP layer (confirmed against source), but exchange_id is effectively required unless data_source names an explicit data file: if data_source is omitted or None, OctoBot defaults it to "current_bot_data" (use a live snapshot of the bot's own current exchange data), which requires a valid exchange_id -- omitting exchange_id in that case fails with a generic BacktestStartFailedError, not MissingExchangeIdError (that error is only raised for an actually-provided-but-unrecognized id; see this module's docstring, "Open question #6"). exchange_id is the same value get_exchange_details returns as its exchange_id field. exchange_type accepts None, "use_current_profile", "spot", "inverse_perpetual", "linear_perpetual", or "margin" (any other value raises inside OctoBot, surfaced as BacktestStartFailedError).

source (both modes) is an optional caller-supplied override for OctoBot's own per-run source identifier; if omitted (the expected case), a fresh UUID is generated and stored as the job's octobot_run_source, then reused automatically for the later get_job_result report fetch -- the caller never needs to manage this value.

Output: {"job_id": str, "state": "queued"} -- always "queued" immediately after this call (the background watcher has not yet had a chance to run at all: asyncio.create_task schedules it, it does not run any of it synchronously). Poll get_job_status(job_id) to observe the transition to "running" and then progress_percent advancing; call get_job_result(job_id) once state == "completed".

Progress notifications (milestone 11, ADR-0002 decision point 3): if this call's MCP request carries a progressToken, the watcher also opportunistically emits an MCP progress notification (progress/total on the same 0-100 scale as progress_percent) each time progress_percent changes -- entirely optional, additive, and never a substitute for polling: get_job_status behaves identically whether or not a token was ever supplied. See octobot_mcp.tools.backtesting's own module-level "Milestone 11" comment block for exactly how this is implemented.

Errors: BacktestStartFailedError for an unknown mode, a missing required field for the chosen mode, or any other start failure; MissingExchangeIdError specifically for current_bot_data mode's confirmed exception. Both are raised from the background watcher (not this function) and surface as the job's errors/state="failed" via get_job_status -- start_backtest itself only raises synchronously for input validation caught before any job is created (unknown mode, missing files), so a bad call never leaves a dangling queued job behind.

get_job_resultA

Fetch a completed backtest job's final report.

Requires state == "completed"; raises JobNotCompletedError naming the current state otherwise -- never returns partial results, even if OctoBot itself would technically serve a report for an in-progress run (or, for compare_strategies, if some but not all children have finished). Raises JobNotFoundError for an unknown job_id (same as get_job_status).

For kind: "backtest", output is {"report": <passthrough>, "trades": <passthrough>} from OctoBot's own GET /backtesting?update_type= backtesting_report response (NFR-8: no renaming/reshaping).

For kind: "compare_strategies" (milestone 10, docs/tasklist.md item 10), output is {"comparison": [...], "diff": ...} -- both already fully computed and stored (once, at completion -- INV-3) by _run_compare_strategies as this job's own result, so this function simply passes them through unchanged; see that function and _build_comparison_entry/_build_diff below for exactly how each is built.

Real report/trades shape, captured live against the OctoBot 2.1.1 test instance this milestone (resolves open question #3) -- data_files mode, DailyTradingMode profile, BTC/EUR on binance, exactly as OctoBot returned it (field names/nesting verbatim, values are example data from that one run):

{
  "report": {
    "bot_report": {
      "starting_portfolio": {"binance": {"BTC": {"available": 10.0, "total": 10.0}, "USDT": {...}}},
      "end_portfolio": {"binance": {"BTC": {...}, "EUR": {...}, "USDT": {...}}},
      "profitability": {"binance": -15.223942726263024},
      "market_average_profitability": {"binance": -16.839423647357034},
      "reference_market": "EUR",
      "trading_mode": "DailyTradingMode"
    },
    "chart_identifiers": [
      {"exchange_id": "...", "exchange_name": "binance", "symbol": "BTC/EUR", "time_frames": ["1m", "1h", "4h", "1d"]}
    ],
    "errors_count": 0,
    "symbol_report": [{"BTC/EUR": 0.8013392638261951}]
  },
  "trades": [
    {
      "SoR": "Simulated", "amount": "7.99127178", "cost": "535066.3103340852",
      "date": "2026-05-02 16:00:00", "exchange": "binance", "fee_cost": "535.0663103340852",
      "fee_currency": "EUR", "id": "b5eb50fd-...", "market": "EUR", "price": "66956.34",
      "ref_market_cost": 535066.3103340853, "symbol": "BTC/EUR", "time": 1777737600,
      "type": "SELL MARKET"
    },
    ...
  ]
}

Confirms the spec's own explicit warning was correct: profitability genuinely exists, but nested three levels deep (report.bot_report.profitability, itself keyed by exchange name, not a bare float) -- NOT at the top level of report as a naive read of the spec's placeholder field list (profitability/max_drawdown/ win_rate) might have assumed; max_drawdown/win_rate were not observed anywhere in this real payload at all. amount/cost/ fee_cost/price in each trade are numeric-looking strings, not JSON numbers (ref_market_cost IS a JSON number) -- passed through exactly as OctoBot sent them (NFR-8), not coerced. This tool does not hardcode or validate against any of these field names anywhere in its own code (per NFR-8/the spec's own instruction not to assume a field list) -- this is documentation of what was observed, not a schema this tool depends on.

compare_strategiesA

Compare 2+ backtest configs sequentially and return one combined job.

Not confirm-gated (ADR-0003): same reasoning as start_backtest -- only ever starts simulations. configs is a list (len(configs) >= 2) of dicts, each shaped exactly like one of start_backtest's own mode-specific input shapes ({"mode": "data_files", "files": [...], ...} or {"mode": "current_bot_data", "exchange_id": ..., ...}), plus an optional "label" key used as that config's own config_label in get_job_result's later comparison/diff output (see _child_config_label) -- stripped before being validated/passed to start_backtest's own mode builders, which know nothing about it. name is accepted (per the spec's input shape) but not otherwise used: neither this tool's own output nor get_job_result's compare_strategies output shape (both fully spec'd) has anywhere to put it.

Every config is validated (same rules start_backtest itself applies) BEFORE any job -- parent or child -- is created, so a bad configs entry never leaves a dangling queued job behind: raises CompareStrategiesStartFailedError naming the offending configs[i] for len(configs) < 2, an unknown mode, a missing required field for that mode, or an unrecognized field name.

Creates one parent job (kind: "compare_strategies") with one child kind: "backtest" job per config (job.child_job_ids, in the same order as configs), then spawns ONE background asyncio.Task (tracked via store.track_watcher_task, same as start_backtest) running _run_compare_strategies, which submits each child sequentially through the shared backtest_execution_lock -- see that function's own docstring for the full sequencing/failure-isolation/cancellation behavior.

Output: {"job_id": str, "state": "queued"|"running", "sub_job_count": int} -- returns immediately, same non-blocking pattern as start_backtest (always "queued" in practice, since asyncio.create_task never runs any of the background task synchronously before this function returns). Poll get_job_status(job_id) for combined progress/ETA and each child's own live state (sub_jobs); call get_job_result(job_id) once state == "completed".

Progress notifications (milestone 11, ADR-0002 decision point 3): if THIS call (the parent) carries a progressToken, notifications report the COMBINED progress across all children (the exact same formula get_job_status already uses for polling reads), never one child's own raw percentage -- see _run_compare_strategies/ _make_compare_strategies_on_tick's own docstrings. Children are never independently callable, so they never carry their own token.

get_tentacle_configA

Fetch one tentacle's current configuration, schema, and display metadata.

Maps to GET /config_tentacle_edit_details/<tentacle> -- see this module's docstring for the spec correction (the spec named GET /config_tentacle, which has no JSON response path at all). tentacle is the tentacle's class name (e.g. "DailyTradingMode"), confirmed live against the OctoBot 2.1.1 test instance.

Returns OctoBot's own JSON body unchanged (NFR-8): {"name": str, "config": object, "displayed_elements": object}. config holds this tentacle's actual configuration values (the same values update_tentacle_config's patch argument would merge into); displayed_elements is the JSON-schema-shaped UI-form metadata OctoBot's own config page renders from, passed through unsimplified.

Read-only, not confirm-gated. An unknown tentacle name raises an uncaught exception inside OctoBot's model layer, surfaced as OctoBotServerError (confirmed live: HTTP 500, plain-text body "Can't find tentacle: <tentacle>", despite a Content-Type: application/json header that doesn't actually describe the body -- handled via this module's _extract_error_text, same as update_tentacle_config).

update_tentacle_configA

Update one tentacle's configuration for the currently active profile.

Maps to POST /config_tentacle?name=<tentacle>&action=update, JSON body = patch (the raw patch dict, unwrapped) -- see this module's docstring for the full source-verified/live-reproduced derivation of this exact shape, including why a distinct batch route (POST /config_tentacles) exists but is deliberately not exposed here.

OctoBot's own success response body is the literal JSON string "<tentacle> updated" (confirmed live: DailyTradingMode, toggled and restored during this milestone's verification). Failure (confirmed live with an unrecognized tentacle name: HTTP 500, plain-text body "Can't find <tentacle> class") is handled via this module's _extract_error_text, since that body is not actually JSON despite the Content-Type: application/json header OctoBot still sends.

Output: {"tentacle": str, "updated_fields": patch, "message": str} -- message is OctoBot's own decoded success string; updated_fields echoes the patch this tool sent (same idiom as octobot_mcp.tools.profiles.update_profile).

Not confirm-gated (ADR-0003): mutates tentacle configuration, does not destroy data, expose credentials, or enable live trading. Raises TentacleConfigUpdateRejectedError on any non-2xx response.

list_tentaclesA

Tier C stub (ADR-0006): tentacle enumeration has no JSON API in OctoBot.

Never makes an HTTP call to OctoBot and never raises -- this is a static, instant response explaining the gap, not a failure (ctx is accepted only for interface consistency with every other registered tool; it is never read). Use get_tentacle_config(tentacle) instead if you already know the tentacle's class name.

get_logsA

Tier C stub (ADR-0006): structured/filterable log listing has no JSON API in OctoBot.

Never makes an HTTP call to OctoBot and never raises -- this is a static, instant response explaining the gap, not a failure (ctx is accepted only for interface consistency with every other registered tool; it is never read). Use export_logs() instead to retrieve OctoBot's full log history as a downloadable zip archive.

export_logsA

Export OctoBot's full logs as a zip archive, base64-encoded.

Maps to GET /export_logs -- the exact same binary-file-download pattern as octobot_mcp.tools.profiles.export_profile (flask.send_file, not JSON): this tool bypasses octobot_mcp.client.request_json for the same reason and base64-encodes the raw response body. See this module's docstring for the source/live verification detail, including why the downloaded filename (unlike export_profile's) is not dynamic.

filename is read from the response's Content-Disposition header via the shared octobot_mcp.tools._shared._parse_content_disposition_filename helper -- the same one export_profile uses -- rather than hardcoding OctoBot's own literal "logs_export.zip" locally.

On failure, OctoBot's own controller (confirmed against source) catches every export exception internally, flashes an HTML-only session message, and 302-redirects back to /logs -- never a 4xx/5xx status. This tool cannot distinguish that redirect from a genuine success at the HTTP-status level alone, so -- the same fail-loud approach export_profile uses for its own missing-header case -- it raises OctoBotMalformedResponseError whenever no parsable filename is found in the response, rather than ever returning a fabricated filename or empty content.

Output: {"filename": str, "content_base64": str}. Read-only, not confirm-gated (ADR-0003).

update_trading_configA

Update the current profile's trading-mode tentacle-activation config.

Maps to POST /config, JSON body {"trading_config": patch} -- see this module's docstring for the full source-verified derivation of this exact shape (the spec's [V]-tagged claim, independently re-confirmed against OctoBot source this session) and the request-body key's literal value (TRADING_CONFIG_KEY = "trading_config"). patch is submitted as-is, unwrapped -- the same shape models.update_tentacles_activation_config (also used by the tentacle/evaluator activation branches of this same route) expects: typically a dict mapping tentacle class names to a boolean (or a "true"/"false" string) activation state.

Output: {"trading_updated_config": <passthrough of OctoBot's own echoed value for this key from its success response>} -- matches the route's own response shape (NFR-8), narrowed to just this one key rather than OctoBot's whole /config response envelope, since every other key in that envelope reflects config domains (tentacle_config, evaluator_config, global_config, removed_elements) this tool never sends and is therefore always that route's own empty-string default.

Not confirm-gated (ADR-0003): mutates strategy/tentacle activation config, does not destroy data, expose credentials, or enable live trading. Raises TradingConfigUpdateRejectedError on any non-2xx response -- see this module's docstring for why, for this tool's own request shape, OctoBot's own error text is confirmed to always be an empty string in practice, never a substantive message, despite the spec's original claim otherwise.

update_evaluator_configA

Update the current profile's evaluator-activation config.

Maps to POST /config, JSON body {"evaluator_config": patch, "deactivate_others": deactivate_others} -- TWO top-level keys, never one nested under the other -- see this module's docstring for the full source-verified derivation of this exact shape and the request-body key's literal value (EVALUATOR_CONFIG_KEY = "evaluator_config", resolving the tool spec's/requirements doc's open question #12). patch is submitted as-is, unwrapped, mapping evaluator class names to a boolean (or "true"/"false" string) activation state -- the same shape update_trading_config submits under its own key, since both reach the same underlying models.update_tentacles_activation_config.

deactivate_others (default False, resolving open question #13): when True, every OTHER currently-active evaluator NOT named in patch is force-disabled -- confirmed by tracing models. update_tentacles_activation_config into TentaclesSetupConfiguration. update_activation_configuration/_deactivate_other_evaluators's own body (see this module's docstring for the full trace); this only ever touches tentacles whose type is one of the four evaluator subtypes (technical-analysis/social/real-time/scripted) -- trading modes and strategies are never affected by this flag. False (this tool's default) is purely additive: only the evaluators named in patch have their activation state changed, every other tentacle is left untouched.

patch's keys are evaluator CLASS NAMES, not display names -- e.g. "RSIMomentumEvaluator", "BBMomentumEvaluator", "DoubleMovingAverageTrendEvaluator", "MACDMomentumEvaluator", "SuperTrendEvaluator" are real examples confirmed against the OctoBot 2.1.1 test instance's default evaluator set. Use this module's own list_evaluators() (task 19, ADR-0009 -- reversed to GO by explicit user decision after originally recommending NO-GO here) to discover every evaluator's exact class name and current activation state: its name field is exactly the string this tool's patch keys must equal, closing the loop structurally. get_tentacle_config(name) does NOT expose activation state (confirmed live: its config field holds only that evaluator's own tunable parameters, e.g. RSI thresholds -- never an enabled/disabled flag).

Output: {"evaluator_updated_config": <passthrough of OctoBot's own echoed value for this key from its success response>} -- matches the route's own response shape (NFR-8), by direct analogy to update_trading_config's own confirmed {"trading_updated_config": ...} shape (same response envelope, different key extracted).

Not confirm-gated (ADR-0003): mutates which evaluators are active, does not destroy data, expose credentials, or enable live trading. Raises EvaluatorConfigUpdateRejectedError on any non-2xx response -- see this module's docstring for why, for this tool's own request shape, OctoBot's own error text is confirmed to always be an empty string in practice (the evaluator_config branch never populates the route's own err_message variable, exactly like the trading_config branch update_trading_config documents).

list_evaluatorsA

List every evaluator OctoBot knows about, including its activation state and category.

Tier-B (ADR-0009, reversed to GO by explicit user decision -- see this module's docstring for the full verified DOM shape and fail-loud assumptions). Closes update_evaluator_config's own companion-read gap: that tool is unusable without already knowing an evaluator's exact class name and current state. Idempotent, read-only, not confirm-gated (ADR-0003).

Output: {"evaluators": [{"name": str, "enabled": bool, "type": str}]}. name is the config-key attribute value -- the same string update_evaluator_config's patch keys must equal, making the closed loop between these two tools a structural guarantee, not a convention to maintain by hand. type is one of "technical_analysis"/"social"/ "realtime"/"scripted", taken from whichever category container an evaluator's row was found inside.

Raises EvaluatorScrapeContractBrokenError (NFR-18) only if zero evaluators are found across ALL FOUR category containers combined, or an expected container is missing from the page entirely -- never for a single category legitimately having zero evaluators (confirmed live: this project's own test instance has zero "Scripted evaluators" installed).

start_data_collectionA

Start a real OctoBot historical data collection run in the background and return immediately.

Not confirm-gated (ADR-0003/tool spec): starts a background download, never touches live trading, destroys no data, exposes no credentials -- same reasoning as start_backtest. Maps to POST /data_collector ?action_type=start_collector [V]. symbols must be non-empty. time_frames is optional (OctoBot collects its own default set when omitted). start_timestamp/end_timestamp are optional epoch milliseconds (confirmed both live and from source this session -- see this module's docstring).

To discover valid exchange/symbols values, use the existing get_all_symbols(exchange) tool (confirmed this session to return data identical to OctoBot's own data-collector-scoped symbol list, per FR-27 -- no separate get_available_symbols_for_collection tool exists here for that reason). For time_frames, use this module's own get_available_timeframes_for_collection(exchange).

Behavior (ADR-0007): creates a kind: "data_collection" job, acquires JobStore.data_collection_execution_lock -- a lock INDEPENDENT of backtest_execution_lock (NFR-15): a concurrent start_backtest + start_data_collection never block each other, but two concurrent start_data_collection calls do serialize (queueing, same as two concurrent backtests) -- then submits the start action and spawns a background watcher connecting to OctoBot's /data_collector Socket.IO namespace, mirroring start_backtest's watcher exactly (this session's own live round-trip confirmation is recorded in this module's docstring).

Output: {"job_id": str, "state": "queued"} -- always "queued" immediately (the background watcher has not yet run at all). Poll get_job_status(job_id) for progress_percent/eta_seconds; get_job_result(job_id) once state == "completed" returns {"exchange", "symbols", "time_frames", "start_timestamp", "end_timestamp", "message"} -- deliberately NO resulting filename (OctoBot itself names none anywhere in this flow, confirmed this session): call list_data_files() afterward to find the new file.

A confirmed OctoBot-side limitation, not a bug in this tool: a collection submitted for a nonexistent/invalid exchange name returns the SAME success response and the SAME "finished" status as a genuinely successful run (confirmed live this session, resolving open question #11) -- if list_data_files() doesn't show the expected new file after a "completed" job, the exchange/symbol/timeframe combination was likely invalid upstream, not a failure this server could have detected sooner. A collection that never progresses at all (staleness, NFR-3) is a separate, detectable case: its eta_confidence will show "low"/eta_seconds: null and it should be cancelled via cancel_job.

Supports progressToken (ADR-0002 decision point 3, reused via octobot_mcp.tools._shared._attach_progress_notifier) exactly like start_backtest.

Errors: DataCollectionStartFailedError wrapping OctoBot's own failure text -- confirmed messages include "Backtesting is disabled.", "Please select an exchange.", "Please select a trading pair.", any of _ensure_backtesting_limits's three possible messages (see this module's docstring -- source-confirmed text, not guessed), and "Can't collect data for {symbols} on {exchange} (Historical data collector is already running)" for OctoBot's own one-at-a-time guard. Raised from the background watcher (not this function), surfacing as the job's errors/state="failed" via get_job_status -- this function itself only raises synchronously for input validation caught before any job is created.

list_data_filesA

List every historical data file already present on the OctoBot instance.

Tier-B (ADR-0008): scrapes the bare GET /data_collector page -- see this module's docstring/_parse_data_collector_html for the verified DOM shape and fail-loud assumptions. Idempotent, read-only, not confirm-gated.

Output: {"files": [{"file": str, "exchange": str, "symbols": [str], "time_frames": [str], "is_full": bool, "start_date": str|null, "end_date": str|null, "date": str|null, "candles_length": int|null}]}. file is the authoritative identifier to pass to delete_data_file. An empty files list is a legitimate result (no data files exist yet) -- distinct from DataFileScrapeContractBrokenError, which means the scrape itself is broken, never silently returned as an empty/partial list (NFR-13).

delete_data_fileA

Permanently delete a historical data file. Confirm-gated (ADR-0003/FR-25).

Deliberately gated, reversed from this addendum's original not-gated draft -- see the tool spec/requirements doc FR-25 for the full reasoning: unlike cancel_order/close_position (instantly correctable), an erroneous delete means re-running a collection that can take minutes to hours, placing this closer to clear_*_history's risk profile. If confirm is not exactly true, no OctoBot call is made at all -- this returns require_confirmation's structured refusal (a normal return, not an error).

Once confirmed, maps to POST /data_collector?action_type= delete_data_file with the raw file string as the JSON body (confirmed against source AND live this session: flask.request. get_json()'s raw value is passed straight through to backtesting_api.delete_data_file(file_name), no wrapping object).

Output on success: {"file": str, "deleted": true, "message": str} (OctoBot's own confirmed live success body: f"{file} deleted").

Errors: DataFileDeleteRejectedError wrapping OctoBot's own f"Can't delete {file_name} ({error})" text -- confirmed verbatim live this session for a nonexistent file ("Can't delete does_not_exist.data (file can't be found)").

import_data_fileA

Upload an existing OctoBot-format historical data file without collecting it via an exchange.

Not confirm-gated (ADR-0003/FR-26): creates new state, destroys nothing. Maps to POST /data_collector?action_type=import_data_file (multipart file field, mirroring create_profile(mode="import_file")'s existing pattern exactly: base64-decode, then a multipart upload).

A confirmed OctoBot-side limitation, not a bug in this tool: this route ALWAYS responds HTTP 200 with the full re-rendered /data_collector page, on both a genuinely saved file and a silently rejected one (confirmed live this session with garbage bytes -- no file appeared in list_data_files() afterward, and the 200 response body carried no inspectable error text anywhere) -- the same invisible-at-the-HTTP-level outcome octobot_mcp.tools.profiles._create_profile_import_file already documents for create_profile(mode="import_file"). This tool therefore determines the outcome via a before/after list_data_files() diff, exactly like that tool does for profiles.

Output: {"filename": str, "imported": true, "message": str}.

Errors: DataFileImportFailedError if file_base64 isn't valid base64, or if no new file appears in list_data_files() after the upload (OctoBot silently rejected it).

get_available_timeframes_for_collectionA

List the time frames OctoBot's data collector supports for one exchange.

Maps to GET /data_collector?action_type=available_timeframes_list &exchange=<name> [V, confirmed live this session]. Read-only, not confirm-gated. No dedicated exception class -- a plain read with no domain-specific failure mode, matching this project's existing convention for read-only tools (e.g. get_all_symbols).

Output: {"time_frames": [str]} -- direct passthrough of OctoBot's own sorted JSON array (NFR-8), e.g. ["1m", "3m", "5m", ..., "1M"].

See this module's docstring for why there is no sibling get_available_symbols_for_collection tool: get_all_symbols(exchange) (already shipped) was confirmed this session to return identical data.

restart_octobotA

Trigger a full OctoBot process restart. Confirm-gated (ADR-0003/ADR-0010).

This is the first tool in this project that can never be made fully safe or graceful (ADR-0010) -- stated plainly, not softened. OctoBot's own restart mechanism (GET /wait_reboot?reboot=true, confirmed against Drakkar-Software/OctoBot source) is an ABRUPT kill-and-re-exec, never the graceful OctoBot.stop() shutdown -- no task cancellation, no explicit order handling occurs before the kill. Exchange-side orders are reconciled from the exchange on reboot (OctoBot's own designed, changelog-hardened behavior, even for live trading), but the exact duration of the unmonitored window during the restart itself is UNVERIFIED (open question #14) and this tool cannot measure or bound it.

Check order (cheapest/safest first, ADR-0010 Decision 4), so a call that's about to be refused never makes an unnecessary live call:

  1. confirm flag. If not exactly true, no OctoBot call is made at all -- this scrapes the currently selected profile's profile_type (the same Tier-B scrape list_profiles/get_profile use) ONLY to build a risk-scaled reason string, then returns require_confirmation's structured refusal (a normal return, not an error). The reason string is more detailed/cautious when profile_type is LIVE -- or the scrape itself is inconclusive, which fails toward the MORE cautious message, never toward silently allowing (ADR-0010 Decision 3) -- than when it is SIMULATOR. This is the SAME confirm=true gate regardless of profile_type: never a hard, non-bypassable block for LIVE profiles (see ADR-0010 for why an earlier draft's hard refusal didn't hold up against OctoBot's own changelog/source evidence that restart-during-live-trading is a designed, hardened scenario).

  2. JobStore active-job check. Refuses outright -- a structured non-exception return, naming every job -- if any backtest/ data_collection job is queued/running (both of octobot_mcp.jobs.JobStore's independent locks checked, ADR-0007's "two independent locks" precedent). Never warns-and-proceeds or auto-cancels (ADR-0010 Decision 4 explicitly rejects both): the caller must cancel_job or wait for completion first.

  3. The one live call. GET /wait_reboot?reboot=true. HTTP 200 -> {"restart_triggered": true, "message": ...} (below). Anything else raises RestartFailedError -- never a false "triggered" result.

Residual risk, accepted (ADR-0010): a job could start in the narrow window between check 2 and the actual reboot call -- accepted given this project's single-MCP-client assumption (A3), not engineered away.

Output on success: {"restart_triggered": true, "message": "OctoBot restart scheduled (~2s delay); the server will be briefly unreachable. Call wait_for_octobot_ready() next, then re-confirm the active profile via get_profile()/list_profiles() before starting a backtest."}.

wait_for_octobot_readyA

Poll for OctoBot's HTTP server becoming reachable again after a restart.

A bounded retry loop against GET /api/version (see this module's docstring for why this specific route was chosen) -- never blocks past timeout_seconds, and never raises for a timeout: that is a normal, honestly-reported result (NFR-3's "honest null/false over a fabricated guess" ethic), matching every other timeout-bounded loop in this project.

Correctness fix (resolving part of open question #14, live-verified against the test instance): a ready: true result now REQUIRES having observed at least one genuinely unreachable poll first. An earlier version of this tool returned ready: true after its very first poll succeeded -- but restart_octobot's own route schedules OctoBot's kill with a confirmed ~2-second delay (models.restart_bot(delay=2), ADR-0010's Context), so a poll issued immediately after restart_octobot returns will almost always land WITHIN that grace window and hit the OLD, not-yet-killed process -- a false positive that completely defeats this tool's purpose. Confirmed live, twice, against the OctoBot 2.1.1 test instance: the HTTP server stayed reachable until ~2.1s post-trigger (matching the confirmed 2s delay almost exactly), then was genuinely unreachable for ~6.3-6.7s, before answering again at ~8.5-8.9s total. A naive first-success-wins loop reported ready: true in ~0.05s both times -- entirely within the pre-kill grace window, never having observed the real restart at all. This loop now tracks whether it has seen a failed poll; a success only counts as ready: true once at least one prior poll in this same call has failed, so ready: true now means "the server went down and came back," not just "answered."

"Ready" (even with the fix above) still means ONLY "the HTTP server answered a request again after a confirmed outage" -- nothing more, and this is now a CONFIRMED gap, not a suspected one. A follow-up live test checked SimpleStrategyEvaluator's default_config/ required_evaluators immediately after a genuine, confirmed restart (dense-polled, not a race) -- unchanged from before the restart. The same end result (restart does not unblock strategy-composition-tuning validation via backtest) was independently confirmed on an entirely separate OctoBot instance/session. Do not present restart_octobot/ wait_for_octobot_ready as a fix for evaluator/strategy-composition tuning validation -- that was this capability's whole stated purpose (ADR-0010's Context) and it does not hold. See ADR-0010's "Negative finding" (Context) and Consequences for the full account, including an open question of whether this capability should be kept at all given its purpose doesn't hold. Order-execution parameters (sizing, stop-loss/take-profit) are unaffected by any of this and apply correctly in a profile_id-targeted backtest without a restart.

DEFAULT_TIMEOUT_SECONDS/DEFAULT_POLL_INTERVAL_SECONDS (60s/2s): the 60s timeout has real headroom above the ~8.5-8.9s measured end-to-end restart time above (measured twice, one instance, one OctoBot version -- some margin is appropriate, not treated as a tight bound); the 2s poll interval reliably samples within the ~6.3-6.7s measured outage window. Still treat both as informed-but-limited defaults (n=2, single instance/version), not a guaranteed bound for every OctoBot deployment -- override both per-call if your own environment differs materially.

Not confirm-gated (a read-only poll). Returns {"ready": true|false, "elapsed_seconds": float, "attempts": int} -- all fields reflect this loop's REAL timing/attempt count, never fabricated.

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources