Handle Errors
Every failure the client can raise is a Backtest360Error. There is one exception type;
you distinguish causes by inspecting its fields rather than catching different classes.
The exception
Backtest360Error carries five fields:
status— the HTTP status from the engine, or0for errors the client raises before a request is sent.code— a stable machine-readable string. Branch on this.body— the engine's response body, when present. For strategy validation it holds the structured error report.request_id— correlates the failure with an engine-side log entry. Include it when reporting a problem.retry_after— seconds to wait before retrying, set on throttling responses.
The full code tables are in the Exceptions reference.
Branch on the code
from backtest360 import Client, Backtest360Error
client = Client()
try:
result = client.backtest(strategy, df, risk=risk)
except Backtest360Error as e:
if e.code in ("AUTH_MISSING", "AUTH_INVALID", "AUTH_EXPIRED"):
print("Check your API key:", e.code)
elif e.status == 422:
# Strategy validation — e.body holds the structured error list.
for err in e.body["errors"]:
print(f"{err['location']}: {err['message']}")
else:
raise
Prefer code over status where you can. A 429, for example, covers three distinct
conditions — a quota (QUOTA_EXCEEDED), too many concurrent requests
(CONCURRENCY_EXCEEDED), and an upstream provider throttle (RATE_LIMITED) — and only
the first carries a retry_after.
What is worth retrying
Retry only transient, server-side conditions. Client and validation errors will not succeed on a second identical attempt.
| Code | Retry? | Notes |
|---|---|---|
QUOTA_EXCEEDED |
Yes | Wait for retry_after before retrying. |
ENGINE_BUSY |
Yes | Compute capacity was exhausted; back off and retry. |
COMPUTE_TIMEOUT |
Yes | The run exceeded the engine's compute window; retry, and consider a smaller request. |
PROVIDER_UNAVAILABLE |
Yes | A data provider was unavailable; back off and retry. |
AUTH_MISSING, AUTH_INVALID, AUTH_EXPIRED |
No | Fix the key. |
INVALID_PARAMETERS, INVALID_SYMBOL, INVALID_LEG_COUNT |
No | Fix the request. |
PLAN_UPGRADE_REQUIRED, SCOPE_DENIED |
No | The key is not entitled to the operation. |
CLIENT_* |
No | The client rejected the input before sending it. |
A retry helper
Honor retry_after when the engine sets it, and fall back to exponential backoff
otherwise.
import time
from backtest360 import Backtest360Error
RETRYABLE = {"QUOTA_EXCEEDED", "ENGINE_BUSY", "COMPUTE_TIMEOUT", "PROVIDER_UNAVAILABLE"}
def with_retry(call, *, attempts=3):
for attempt in range(attempts):
try:
return call()
except Backtest360Error as e:
if e.code not in RETRYABLE or attempt == attempts - 1:
raise
delay = e.retry_after if e.retry_after is not None else 2 ** attempt
time.sleep(delay)
result = with_retry(lambda: client.backtest(strategy, df, risk=risk))
!!! note
Retries multiply your quota usage. Cap the attempt count and always respect
retry_after so you do not compound a throttle.