| 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: 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).
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.
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. |