View as markdown
Getting started

Agents

The shape of this API is the reason it works natively with coding agents. Flat SQL WHERE clauses over named columns and flags. No joins, no window functions, no schema introspection round-trip. Hand the schema to your agent once and it can compose, backtest, and subscribe queries on its own. This page covers the conceptual setup and the drop-in tool definitions for every common runtime.

Why this works with agents

Constraints chosen for LLM ergonomics, not against them.

  • The grammar is small. A WHERE clause over named columns. No JOIN, no GROUP BY, no OVER. An LLM that has seen the schema once can compose the query without a doc lookup.
  • The schema fits in a prompt. ~100 numeric and string columns + ~50 pre-thresholded boolean flags. The full schema page is one scroll. Drop it into a system prompt and the agent has the vocabulary.
  • No surprise dialect. Identifiers are the customer-facing names listed on the schema page; the API doesn't rewrite, alias, or coerce them. What the agent writes is what runs.
  • One language, every endpoint. The same q string drives /scan, /scan?asof=, /webhooks, and the saved-rules surface. The agent learns one composition rule and applies it everywhere.

The typical agent loop

Three calls, all the same shape.

  1. User asks something in English (“find oversold semis bouncing on volume”).
  2. Agent translates to a q clause and calls /v2/scan to see live matches.
  3. If the user wants confidence, agent calls /v2/scan?asof=YYYY-MM-DD across past dates to sanity-check the edge.
  4. If the user wants to be alerted, agent registers the same q as a webhook so future matches push to the user's channel.

State lives in the API. The agent stays stateless across turns; the user's saved universes, rules, and webhooks persist on Tickerbot.

What to put in the system prompt

A short bootstrap so the agent doesn’t have to re-derive the vocabulary.

Three things turn an LLM into a useful Tickerbot agent:

  1. The list of column and flag names (from /api/schema.json, or just the schema page pasted in).
  2. The grammar bounds (“flat WHERE clause; identifiers are bare names; numeric literals are fully specified, not 1.5B”).
  3. The asset-symbol prefix rules (X:BTCUSD for crypto, etc. — see Asset symbols).

See the marketing-side /agents page for a worked example with prompts, an agent transcript, and a starter repo.

Pick a runtime

Claude tool-use docs for the API surface. Pass the JSON below as the tools array on a Messages request.

All tools, one JSON blob

The whole set in one paste. Use this if your runtime accepts a tool array.

40 tools · Claude tool use
[
  {
    "name": "tickerbot_list_tickers",
    "description": "GET /v2/tickers. List active tickers, paginated. Returns the universe of tracked symbols (~12,000 US equities + top 100 crypto by market cap) in alphabetical order with a slim payload (`ticker`, `name`, `asset_type`, `exchange`, `market_cap`). Use cursor pagination to walk the full list, or pass `?tickers=...` for bulk lookup of named symbols with full rows.",
    "input_schema": {
      "type": "object",
      "properties": {
        "tickers": {
          "type": "string",
          "description": "Comma-separated list of symbols (up to 50). When set, the response returns the full row for each requested symbol; pagination params are ignored. Symbols missing from our universe come back with `_not_found: true`."
        },
        "limit": {
          "type": "integer",
          "description": "Page size for the alphabetical walk. Max 1000.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from the previous response's `next_cursor` field. Continues the walk from after that page."
        },
        "search": {
          "type": "string",
          "description": "Substring filter over `ticker` (case-insensitive) and `name`. When set, results are ordered by `market_cap DESC NULLS LAST`."
        },
        "asset_type": {
          "type": "string",
          "description": "Filter by asset type — typically `equity` or `crypto`. Matched case-insensitively against the stored value."
        },
        "exchange": {
          "type": "string",
          "description": "Filter by exchange code (uppercase). Common values: `XNYS`, `XNAS`, `BATS`."
        },
        "sector": {
          "type": "string",
          "description": "Filter by sector name. Exact match against the stored `sector` field."
        },
        "min_market_cap": {
          "type": "number",
          "description": "Minimum market cap (USD). Results are ordered by `market_cap DESC NULLS LAST` when set."
        }
      }
    }
  },
  {
    "name": "tickerbot_get_ticker",
    "description": "GET /v2/tickers/{ticker}. Get the full current state of one ticker. Returns the entire ticker row. Every field on the schema page, including current values for all numeric signals and the current value of every boolean flag. Treat it as the authoritative current snapshot for a symbol.",
    "input_schema": {
      "type": "object",
      "properties": {
        "ticker": {
          "type": "string",
          "description": "Ticker symbol. Case-insensitive. Equities: exchange symbol (`AAPL`). Crypto: bare symbol (`BTC`)."
        }
      },
      "required": [
        "ticker"
      ]
    }
  },
  {
    "name": "tickerbot_get_ticker_history",
    "description": "GET /v2/tickers/{ticker}/history. Get the full ticker row as of a past date. Time-travel to any historical date and pull the whole wide row as it stood then, including the boolean flag values, technical indicators, and the most-recent fundamentals known on that date. Useful for backtests and \"what did we know\" reconstructions.",
    "input_schema": {
      "type": "object",
      "properties": {
        "ticker": {
          "type": "string",
          "description": "Ticker symbol."
        },
        "asof": {
          "type": "string",
          "description": "Target date as `YYYY-MM-DD` or full ISO timestamp `YYYY-MM-DDTHH:MM:SSZ`. The response returns the most-recent daily snapshot on or before this date."
        }
      },
      "required": [
        "ticker",
        "asof"
      ]
    }
  },
  {
    "name": "tickerbot_get_ticker_events",
    "description": "GET /v2/tickers/{ticker}/events. Discrete event log for one ticker. Returns splits, dividends, and analyst rating changes for the ticker, newest-first, in a unified envelope. Each event has a `ts`, a `kind`, and a `payload` whose shape depends on the kind.",
    "input_schema": {
      "type": "object",
      "properties": {
        "ticker": {
          "type": "string",
          "description": "Ticker symbol."
        },
        "from": {
          "type": "string",
          "description": "Earliest event timestamp (inclusive). YYYY-MM-DD or ISO. Defaults to your plan's history window."
        },
        "to": {
          "type": "string",
          "description": "Latest event timestamp (inclusive). YYYY-MM-DD or ISO. Defaults to \"now\"."
        },
        "kind": {
          "type": "string",
          "description": "Filter by event kind.",
          "enum": [
            "split",
            "dividend",
            "rating_change"
          ]
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 1000.",
          "default": 100
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from the previous response."
        }
      },
      "required": [
        "ticker"
      ]
    }
  },
  {
    "name": "tickerbot_list_signals_catalog",
    "description": "GET /v2/signals. Built-in signals plus your custom signals, each tagged with `kind`. Returns the unified signal catalog: every built-in column on the live schema (tagged `kind: 'builtin'`, carrying `name`, `type`, `source`) plus the caller's own custom signals (tagged `kind: 'expression'`, carrying `name`, `description`, `expr`, `created_at`, `updated_at`).\n\nPagination applies only to the custom-signal slice — built-ins are returned in their entirety on every page since they're a small fixed set.",
    "input_schema": {
      "type": "object",
      "properties": {
        "kind": {
          "type": "string",
          "description": "Filter by kind. Omit to return both.",
          "enum": [
            "builtin",
            "custom"
          ]
        },
        "limit": {
          "type": "integer",
          "description": "Page size for the custom-signal slice. Max 200.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from a prior response."
        }
      }
    }
  },
  {
    "name": "tickerbot_get_signals_match",
    "description": "GET /v2/signals/{signal}. Find tickers that match a signal right now. Return the set of tickers where the named signal is true (for boolean flags) or where the numeric value satisfies a single-bounded condition. Sorted by signal value descending for numerics, alphabetically by ticker for booleans.",
    "input_schema": {
      "type": "object",
      "properties": {
        "signal": {
          "type": "string",
          "description": "A column on `ticker`. Boolean flags (e.g. `golden_cross_today`, `above_sma_50`) are detected automatically; numerics (e.g. `rsi_14`, `market_cap`, `pe_ratio`) require a condition."
        },
        "condition": {
          "type": "string",
          "description": "Required for numeric signals. Single bound, format `<op><value>`. Operators: `>`, `>=`, `=`, `!=`, `<`, `<=`. Examples: `>70`, `<=200`, `!=0`. Ignored for boolean flags."
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 200.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from the previous response."
        }
      },
      "required": [
        "signal"
      ]
    }
  },
  {
    "name": "tickerbot_get_signal_history",
    "description": "GET /v2/signals/{signal}/{ticker}/history/{interval}. Time series of one signal for one ticker. Returns a sequence of `{t, v}` bars for the requested signal × ticker at the chosen resolution. `t` is an ISO timestamp (or `YYYY-MM-DD` for daily/weekly). `v` is the signal value at that bar (a number for numeric signals, `true`/`false` for boolean flags).",
    "input_schema": {
      "type": "object",
      "properties": {
        "signal": {
          "type": "string",
          "description": "A column on the wide-state tables (matches the schema page 1:1)."
        },
        "ticker": {
          "type": "string",
          "description": "Ticker symbol."
        },
        "interval": {
          "type": "string",
          "description": "Resolution. Daily-only signals (SMAs, RSI, MACD, fundamentals) are not stored at minute/hourly resolution; request `1d` for those.",
          "enum": [
            "1m",
            "1h",
            "1d",
            "1w"
          ]
        },
        "from": {
          "type": "string",
          "description": "Earliest bar timestamp (inclusive). YYYY-MM-DD or ISO. Defaults to your plan's history window."
        },
        "to": {
          "type": "string",
          "description": "Latest bar timestamp (inclusive). YYYY-MM-DD or ISO. Defaults to \"now\"."
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 1000.",
          "default": 252
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor. Walks back in time page-by-page from the most-recent bar."
        }
      },
      "required": [
        "signal",
        "ticker",
        "interval"
      ]
    }
  },
  {
    "name": "tickerbot_create_custom_signal",
    "description": "POST /v2/signals. Save a named SQL expression you can reference like a built-in signal. Persists a named boolean (or numeric) expression in the existing predicate grammar. Once saved, the name is referenceable from any SQL context — rule `q`, strategy `entry`, exit `signal_off`/`signal_on`, leg `what.scan` — and the predicate compiler inlines its body before SQL emit.\n\nThe expression is compiled at create time against the live signal column whitelist; references to other custom signals you own are inlined first (recursion is detected). Names are scoped per-user: `(user_id, name)` is the key. Names cannot collide with built-in column names — those return 409 `name_collision`.",
    "input_schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Slug — `^[a-z][a-z0-9_]{0,63}$`. Must not collide with any built-in column name."
        },
        "expr": {
          "type": "string",
          "description": "SQL expression in the predicate grammar. May reference built-in columns and other custom signals you own. Max 4000 chars."
        },
        "description": {
          "type": "string",
          "description": "Free-form notes. Max 500 chars."
        }
      },
      "required": [
        "name",
        "expr"
      ]
    }
  },
  {
    "name": "tickerbot_patch_custom_signal",
    "description": "PATCH /v2/signals/{name}. Edit the expression or description of one of your custom signals. Partial update — supply `expr`, `description`, or both. Providing `expr` recompiles against the live column whitelist (same validator as create). Built-in signals are read-only — only custom signals you own can be patched.",
    "input_schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Custom signal slug."
        },
        "expr": {
          "type": "string",
          "description": "New SQL expression. Re-validated and re-inlined against your other custom signals."
        },
        "description": {
          "type": "string",
          "description": "New description."
        }
      },
      "required": [
        "name"
      ]
    }
  },
  {
    "name": "tickerbot_delete_custom_signal",
    "description": "DELETE /v2/signals/{name}. Remove a custom signal — refused by default if anything references it. By default, DELETE is cascade-safe: it refuses with 409 `signal_referenced` if any of your strategies (or other custom signals) reference this signal by name. The response carries `referencing_strategies` and `referencing_signals` arrays so you can audit before deciding.\n\nPass `?force=true` to delete anyway. Existing references will compile to `unknown_column` errors next time the consumer recompiles, so use force when you've already fixed the references.",
    "input_schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Custom signal slug."
        },
        "force": {
          "type": "boolean",
          "description": "When `true`, skip the reference check and delete. References will break on next recompile.",
          "default": "false"
        }
      },
      "required": [
        "name"
      ]
    }
  },
  {
    "name": "tickerbot_get_scan",
    "description": "GET /v2/scan. Run a SQL WHERE against the current universe. Filter the live ticker universe with a SQL WHERE clause expressed in our SQL grammar. Returns matching tickers sorted by your chosen column. Columns in `q`, `order`, and `fields` are the customer-facing names listed on /api/schema and /api/flags. No translation layer.",
    "input_schema": {
      "type": "object",
      "properties": {
        "q": {
          "type": "string",
          "description": "SQL WHERE expression. Required unless `rule` is supplied. Max 4000 chars. Semicolons, comments, and write-side keywords (INSERT/UPDATE/DELETE/DROP/etc.) are rejected."
        },
        "rule": {
          "type": "string",
          "description": "Slug of a saved rule (see `/v2/rules`). Its `q`, `universe_id`, `order`, `dir`, and `fields` become defaults; any of those passed as query-string parameters override."
        },
        "universe": {
          "type": "string",
          "description": "Slug of a system universe (`top_10`, `top_100`) or one of your own (`/v2/universes`). Scopes the scan to those tickers. When omitted, the scan runs across all ~12,000 tracked tickers."
        },
        "asof": {
          "type": "string",
          "description": "Optional. Run the WHERE against historical daily state for the given `YYYY-MM-DD` (or full ISO). Plan history depth applies."
        },
        "order": {
          "type": "string",
          "description": "Column to sort by. Must be a valid identifier (lowercase letters, digits, underscore).",
          "default": "day_change_pct"
        },
        "dir": {
          "type": "string",
          "description": "Sort direction.",
          "enum": [
            "asc",
            "desc"
          ],
          "default": "desc"
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated list of extra columns to include in each result row. Default columns are always present: ticker, name, asset_type, price, day_change_pct, gap_pct, relative_volume, market_cap."
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 100.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from the previous response."
        }
      }
    }
  },
  {
    "name": "tickerbot_post_scan",
    "description": "POST /v2/scan. Same as GET /v2/scan, with `q` in the body. POST mirror of `GET /v2/scan` for queries too long to fit in a URL. Accepts the same parameters (`q`, `order`, `dir`, `fields`, `limit`) in a JSON body. `cursor` continues to come from the query string for consistency with the GET pagination shape.",
    "input_schema": {
      "type": "object",
      "properties": {
        "q": {
          "type": "string",
          "description": "SQL WHERE expression."
        },
        "order": {
          "type": "string",
          "description": "Sort column."
        },
        "dir": {
          "type": "string",
          "description": "Sort direction.",
          "enum": [
            "asc",
            "desc"
          ]
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated extra columns."
        },
        "limit": {
          "type": "integer",
          "description": "Page size, max 100."
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor (query string, not body)."
        }
      },
      "required": [
        "q"
      ]
    }
  },
  {
    "name": "tickerbot_post_webhook",
    "description": "POST /v2/webhooks. Create a webhook subscription. Register a SQL WHERE clause (inline `q` or `rule_id`) + an HTTPS target URL. On the cadence configured for your plan, we evaluate the clause against the relevant ticker universe. When a new ticker enters the match set, we POST a `webhook.fired` event to your target. On creation we also send a one-shot `webhook.ping` to verify reachability; the subscription stays in `pending_verification` until the ping returns a 2xx response, then flips to `active`.",
    "input_schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Human-readable label. Helps you find this webhook later in `GET /v2/webhooks`. Doesn't need to be unique."
        },
        "q": {
          "type": "string",
          "description": "SQL WHERE clause. Same grammar as `/v2/scan`. Either this or `rule_id` is required."
        },
        "rule_id": {
          "type": "string",
          "description": "Slug of a saved rule (see `/v2/rules`). The webhook stores a snapshot of the rule's `q` + `universe_id` at creation time."
        },
        "target_url": {
          "type": "string",
          "description": "HTTPS endpoint that will receive POSTed events. Must start with `https://`. Plain HTTP is rejected."
        },
        "cadence": {
          "type": "string",
          "description": "How often to evaluate this webhook. Defaults to your plan's fastest cadence; pick a slower one to conserve quota or smooth out noisy fires.",
          "enum": [
            "1m",
            "5m",
            "15m",
            "hourly",
            "nyse_open"
          ]
        }
      },
      "required": [
        "name",
        "target_url"
      ]
    }
  },
  {
    "name": "tickerbot_list_webhooks",
    "description": "GET /v2/webhooks. List webhook subscriptions on this API key. Returns webhook subscriptions created by the current API key, newest-first. Visibility is key-scoped: webhooks created with a different key on the same account are not returned here. Use `?status=` to filter and the `cursor` field to paginate.",
    "input_schema": {
      "type": "object",
      "properties": {
        "status": {
          "type": "string",
          "description": "Filter by status.",
          "enum": [
            "active",
            "pending_verification",
            "disabled"
          ]
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 100.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from the previous response."
        }
      }
    }
  },
  {
    "name": "tickerbot_get_webhook",
    "description": "GET /v2/webhooks/{id}. Fetch one webhook subscription. Returns the current state of a webhook subscription. Visibility is key-scoped: webhooks created by another key (even on the same account) return 404.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Webhook id returned by `POST /v2/webhooks`."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_delete_webhook",
    "description": "DELETE /v2/webhooks/{id}. Delete a webhook subscription. Hard delete. Any pending deliveries already enqueued may still fire (they capture the target URL + signing secret at queue time). To pause-rather-than-delete, just stop accepting deliveries at your target; after enough consecutive failures the subscription flips to `disabled`.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Webhook id."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_list_webhook_deliveries",
    "description": "GET /v2/webhooks/{id}/deliveries. Inspect recent deliveries for a webhook. Returns the most-recent deliveries (pings and fires) for a webhook, newest-first. Useful for diagnosing failures and verifying signatures. The full request body is not included; we keep them small.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Webhook id."
        },
        "status": {
          "type": "string",
          "description": "Filter by delivery status.",
          "enum": [
            "pending",
            "delivered",
            "permanent_failure"
          ]
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 100.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_enable_webhook",
    "description": "POST /v2/webhooks/{id}/enable. Re-enable a disabled or pending webhook. Resets a webhook back to `pending_verification`, clears its match-state cache (so the next eval treats every currently-matching ticker as new and fires a single `webhook.fired`), and re-enqueues the handshake ping. On 2xx the webhook flips to `active`.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Webhook id."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_test_fire_webhook",
    "description": "POST /v2/webhooks/{id}/test. Send a one-off synthetic delivery for QA / smoke testing. Writes a pending `webhook.fired` delivery with `test: true` and an empty `matches` array. The scheduler picks it up on its next tick and POSTs the same headers + signature shape as a real firing — the only difference is the `test` flag in the body and on the delivery row. Useful for verifying your receiver is reachable, parses the payload correctly, and verifies the signature.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Webhook id."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_list_universes",
    "description": "GET /v2/universes. List your universes. A universe is a named list of tickers you can reference from `/v2/scan?universe=`, `/v2/signals/{signal}?universe=`, `/v2/tickers?universe=`, and `/v2/webhooks`. System universes (`top_10` and `top_100`, the top-N most-actively-traded tickers, rebalanced monthly) are available to every account at `GET /v2/universes/system`.",
    "input_schema": {
      "type": "object",
      "properties": {
        "limit": {
          "type": "integer",
          "description": "Page size. Max 100.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from the previous response."
        }
      }
    }
  },
  {
    "name": "tickerbot_create_universe",
    "description": "POST /v2/universes. Create a universe. Create a named ticker list owned by the calling account. Body: `{ id?, name, description?, tickers }`. If `id` is omitted, a slug is generated.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Optional slug. Lowercase letters, digits, underscore. Must be unique within your account."
        },
        "name": {
          "type": "string",
          "description": "Human-readable label."
        },
        "description": {
          "type": "string",
          "description": "Free-form notes."
        },
        "tickers": {
          "type": "array",
          "description": "Ticker symbols. Validated against your plan scope + the active universe.",
          "items": {
            "type": "string"
          }
        }
      },
      "required": [
        "name",
        "tickers"
      ]
    }
  },
  {
    "name": "tickerbot_list_system_universes",
    "description": "GET /v2/universes/system. List system universes (top_10, top_100). System universes are read-only ticker lists rebalanced monthly by 30-day trailing dollar volume. Available to every account regardless of plan.",
    "input_schema": {
      "type": "object",
      "properties": {}
    }
  },
  {
    "name": "tickerbot_get_universe",
    "description": "GET /v2/universes/{id}. Fetch a single universe. Returns the universe doc. Use `top_10`/`top_100` to fetch a system universe; any other slug must be one your account owns.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Universe slug."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_patch_universe",
    "description": "PATCH /v2/universes/{id}. Update a universe. Update `name`, `description`, or `tickers`. To add/remove ticker subsets without replacing the full list, pass `{ add: [...], remove: [...] }` instead of `tickers`.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Universe slug."
        },
        "name": {
          "type": "string",
          "description": "New label."
        },
        "description": {
          "type": "string",
          "description": "New notes."
        },
        "tickers": {
          "type": "array",
          "description": "Replace the full ticker list.",
          "items": {
            "type": "string"
          }
        },
        "add": {
          "type": "array",
          "description": "Add these tickers (deduplicated).",
          "items": {
            "type": "string"
          }
        },
        "remove": {
          "type": "array",
          "description": "Remove these tickers.",
          "items": {
            "type": "string"
          }
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_delete_universe",
    "description": "DELETE /v2/universes/{id}. Delete a universe. Permanently delete one of your universes. System universes cannot be deleted.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Universe slug."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_list_rules",
    "description": "GET /v2/rules. List your saved rules. A rule is a saved `{q, universe_id?, order, dir, fields}` bundle. Rules can be referenced from `/v2/scan?rule=` (one-off run) and `/v2/webhooks` (subscribe to fires). Same SQL grammar as `/v2/scan`.",
    "input_schema": {
      "type": "object",
      "properties": {
        "limit": {
          "type": "integer",
          "description": "Page size. Max 100.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor."
        }
      }
    }
  },
  {
    "name": "tickerbot_create_rule",
    "description": "POST /v2/rules. Create a saved rule. Save a scan bundle so it can be re-run with `/v2/scan?rule=` or subscribed to via `/v2/webhooks`. The `universe_id` is optional; if unset, the rule runs against your plan default at execution time.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Optional slug."
        },
        "name": {
          "type": "string",
          "description": "Human-readable label."
        },
        "description": {
          "type": "string",
          "description": "Free-form notes."
        },
        "q": {
          "type": "string",
          "description": "SQL WHERE clause."
        },
        "universe_id": {
          "type": "string",
          "description": "Slug of a system or caller-owned universe to scope this rule."
        },
        "order": {
          "type": "string",
          "description": "Sort column.",
          "default": "day_change_pct"
        },
        "dir": {
          "type": "string",
          "description": "Sort direction.",
          "enum": [
            "asc",
            "desc"
          ]
        },
        "fields": {
          "type": "array",
          "description": "Extra columns to include.",
          "items": {
            "type": "string"
          }
        }
      },
      "required": [
        "name",
        "q"
      ]
    }
  },
  {
    "name": "tickerbot_get_rule",
    "description": "GET /v2/rules/{id}. Fetch a saved rule. Returns the rule doc.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Rule slug."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_patch_rule",
    "description": "PATCH /v2/rules/{id}. Update a saved rule. Update any field on a rule. To detach `universe_id` set it to `null`.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Rule slug."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_delete_rule",
    "description": "DELETE /v2/rules/{id}. Delete a saved rule. Webhooks referencing this rule continue to fire using their snapshotted `q`; they don't break.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "Rule slug."
        }
      },
      "required": [
        "id"
      ]
    }
  },
  {
    "name": "tickerbot_create_strategy",
    "description": "POST /v2/strategies. Create a multi-leg strategy. Persist a strategy under your account. The body carries five top-level fields: `name` (slug), `description` (free-form), `allocation` (the dollar pot legs size against), `cadence` (when the runner ticks the strategy), and `legs` (one or more rules — what to trade, when to enter, when to exit, how much).\n\nA leg's `what` is one of three forms: `{ ticker: \"AAPL\" }`, `{ universe: \"<slug>\" }`, or `{ scan: \"<SQL WHERE>\" }`. `entry` is either a bare SQL string (single-tranche, simple case) or an array of `{ condition, weight }` objects for scaling in. `exits` is an ordered, first-match-wins list of typed exits (10 types — see notes). `sizing` picks one of `fixed_fractional`, `risk_based`, or `vol_targeted`. `max_concurrent_positions` caps how many tickers the leg can hold at once (default 1).\n\nEvery embedded SQL expression — `what.scan`, `entry` conditions, `signal_off`/`signal_on` exits — is compiled at create time against the live signal column whitelist. Compile errors are returned as a 400 with the failing path. Created strategies start in `status: 'stopped'` — flip to `'running'` via PATCH when you're ready for the runner to tick it.",
    "input_schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Slug — `^[a-z][a-z0-9_]{0,63}$`, unique per account."
        },
        "description": {
          "type": "string",
          "description": "Free-form notes. Max 500 chars."
        },
        "allocation": {
          "type": "number",
          "description": "Dollar pot the legs size against. Positive number."
        },
        "cadence": {
          "type": "string",
          "description": "Either `{ type: \"recurring\", interval: \"1m\"|\"5m\"|\"15m\"|\"1h\"|\"1d\", at?: \"bar_close\"|\"open\"|\"close\"|\"premarket\" }` or `{ type: \"one_time\", after: <ISO timestamp or epoch> }`."
        },
        "legs": {
          "type": "string",
          "description": "One or more legs. Each leg: `{ name?, side: \"long\"|\"short\", what, entry, exits?, sizing, max_concurrent_positions? }`. See description and STRATEGY_DESIGN.md §4 for full shape."
        }
      },
      "required": [
        "name",
        "allocation",
        "cadence",
        "legs"
      ]
    }
  },
  {
    "name": "tickerbot_get_strategy",
    "description": "GET /v2/strategies/{name}. Fetch a saved strategy. Returns the full strategy definition for the calling user — same shape POST returns. Soft-deleted strategies are still accessible by name (their `status` field is `\"deleted\"`).",
    "input_schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Strategy slug."
        }
      },
      "required": [
        "name"
      ]
    }
  },
  {
    "name": "tickerbot_list_strategies",
    "description": "GET /v2/strategies. List your strategies, newest first. Cursor-paginated. Sort: `created_at` desc, then `name` asc. Soft-deleted strategies are excluded unless you pass `?status=deleted`.",
    "input_schema": {
      "type": "object",
      "properties": {
        "status": {
          "type": "string",
          "description": "Filter by status.",
          "enum": [
            "running",
            "stopped",
            "ended",
            "deleted"
          ]
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 200.",
          "default": 50
        },
        "cursor": {
          "type": "string",
          "description": "Opaque cursor from a prior response."
        }
      }
    }
  },
  {
    "name": "tickerbot_patch_strategy",
    "description": "PATCH /v2/strategies/{name}. Partial update — change any subset of fields, including status. Any of `description`, `allocation`, `cadence`, `legs`, and `status` may be supplied; missing fields are unchanged. Providing `legs` recompiles every embedded SQL expression — same validator as POST. Use `status` to start or stop the runner: `'running'` makes the strategy eligible to tick, `'stopped'` pauses it (open positions persist and stay individually closeable). Use DELETE to soft-delete.",
    "input_schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Strategy slug (immutable)."
        },
        "description": {
          "type": "string",
          "description": "New description. Max 500 chars."
        },
        "allocation": {
          "type": "number",
          "description": "New dollar pot. Positive number."
        },
        "cadence": {
          "type": "string",
          "description": "Replace the cadence. Same shape as create."
        },
        "legs": {
          "type": "string",
          "description": "Replace the legs array. Every SQL expression is recompiled."
        },
        "status": {
          "type": "string",
          "description": "Set runner state.",
          "enum": [
            "running",
            "stopped"
          ]
        }
      },
      "required": [
        "name"
      ]
    }
  },
  {
    "name": "tickerbot_delete_strategy",
    "description": "DELETE /v2/strategies/{name}. Soft-delete a strategy; optionally force-close open positions. By default, DELETE soft-deletes: the runner stops evaluating, but the strategy record and its positions remain queryable and individually closeable. This is the default because force-closing real positions on an accidental DELETE would be costly.\n\nPass `?force=true` to soft-delete and force-close every open position belonging to this strategy in one call. Closed positions get `exit_reason: 'force_close'` and `shares_remaining: 0`.",
    "input_schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Strategy slug."
        },
        "force": {
          "type": "boolean",
          "description": "When `true`, also force-close all open positions for this strategy.",
          "default": "false"
        }
      },
      "required": [
        "name"
      ]
    }
  },
  {
    "name": "tickerbot_list_positions",
    "description": "GET /v2/strategies/{name}/positions. Positions opened by the strategy's runner. Lists positions belonging to a strategy. The position table is Tickerbot's own ledger — it records what the strategy did, not what's actually held at a brokerage. Position rows carry `entry_price` (shares-weighted across filled tranches), `entry_atr`, `peak_price`, share counts, `tranches_fired`, `realized_pnl_dollars`, and `exit_reason` (set when closed).",
    "input_schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Strategy slug."
        },
        "status": {
          "type": "string",
          "description": "Filter by open/closed.",
          "enum": [
            "open",
            "closed",
            "all"
          ],
          "default": "open"
        },
        "limit": {
          "type": "integer",
          "description": "Page size. Max 500.",
          "default": 100
        }
      },
      "required": [
        "name"
      ]
    }
  },
  {
    "name": "tickerbot_close_position",
    "description": "POST /v2/strategies/{name}/positions/{id}/close. Manually close (or partially close) an open position. Force-close a position at a caller-supplied price. The endpoint marks the position closed (or scales it down via `size_pct`), updates `shares_remaining` and `realized_pnl_dollars`, and sets `exit_reason: 'manual'` when the position is fully closed.\n\nPnL math: `(price - entry_price) * shares_to_close * direction` where `direction` is +1 for long, -1 for short. `price` is supplied by the caller because the v1 runner does not yet have a live-quote integration; the caller is expected to pass the current market price (or a fill price from an external broker).",
    "input_schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Strategy slug."
        },
        "id": {
          "type": "string",
          "description": "Position UUID — see the list-positions endpoint."
        },
        "price": {
          "type": "number",
          "description": "Close price. Positive number."
        },
        "size_pct": {
          "type": "number",
          "description": "Fraction of remaining shares to close, in (0, 100]. Default 100 = full close.",
          "default": 100
        }
      },
      "required": [
        "name",
        "id",
        "price"
      ]
    }
  },
  {
    "name": "tickerbot_run_tick",
    "description": "POST /v2/strategies/{name}/run-tick. Evaluate the strategy once against the latest bar. Runs a single tick of the live runner: evaluates exits on any open positions for this strategy at the latest bar (first-match-wins per position), then evaluates tranche entries on still-open positions, then opens new positions on candidates that pass entry and fit under `max_concurrent_positions`. Lifecycle changes are written to the position ledger.\n\nIntended for: (a) orchestration calls from Claude Code, (b) manual \"fire this strategy now\" UX, (c) a future scheduler. Auto-running on cadence is not yet wired — until it is, `run-tick` is how a strategy executes.",
    "input_schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Strategy slug."
        }
      },
      "required": [
        "name"
      ]
    }
  },
  {
    "name": "tickerbot_strategy_backtest",
    "description": "GET /v2/strategies/{name}/backtest. Walk historical bars and return trades + equity curve + summary. Runs the strategy through historical bars at its own configured cadence interval (the backtest does **not** take an `interval` override — backtesting at a cadence different from the one the strategy is configured for produces results that don't reflect how the strategy would actually run). Entries fire on false→true transitions; exits evaluate first each bar, first-match-wins. PnL is computed per fill with `cost_bps` applied to both sides.\n\nReturns `{ trades, equity_curve, summary }` per STRATEGY_DESIGN.md §7. The equity curve enables Sharpe + drawdown calculation and dashboard charts; the summary is what the dashboard's metric strip displays.\n\n**MVP scope.** The engine currently supports: single-leg strategies, single-ticker `what` only, `fixed_fractional` sizing only, single-string (non-tranched) entry, full-close exits (`size_pct = 100`), and exit types `stop_loss_pct`, `take_profit_pct`, `trailing_stop_pct`, `max_hold_bars`, `signal_off`, `signal_on`. Long is fully supported; short is recognised but not yet wired. Strategies outside this envelope get a 400 `not_supported` naming the first unsupported feature.",
    "input_schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Strategy slug."
        },
        "from": {
          "type": "string",
          "description": "Earliest bar (inclusive). ISO timestamp or epoch ms. Defaults to 90 days ago."
        },
        "to": {
          "type": "string",
          "description": "Latest bar (inclusive). ISO timestamp or epoch ms. Defaults to now."
        },
        "cost_bps": {
          "type": "number",
          "description": "Round-trip cost in basis points, applied to both sides of every fill. Non-negative.",
          "default": 5
        }
      },
      "required": [
        "name"
      ]
    }
  },
  {
    "name": "tickerbot_list_news_scan",
    "description": "GET /v2/news/scan. SQL-style scan of the news_article archive — article rows or aggregate rollups. One endpoint, two shapes. With no `group_by` you get article rows; with `group_by` you get rollups (counts, averages) grouped by your expressions. Auto-joins `UNNEST(tickers) AS tk` whenever any clause references the `tk` alias — that's how you write per-ticker rollups without unnesting yourself.\n\nThere is no separate \"asof\" parameter. Historical queries are just WHERE filters on `time_published`:\n\n```\nq=time_published >= '2026-01-15' AND time_published < '2026-01-16'\nq=time_published >= NOW() - INTERVAL '7 days'\n```\n\nThe grammar is the same SQL whitelist used by `/v2/scan`: comparison and logical operators, casts (`::numeric`, `::date`, …), array operators (`= ANY(tickers)`, `tickers && ARRAY[…]`), JSONB operators (`->`, `->>`, `@>`), and the scalar/aggregate functions listed below. Subqueries and DDL keywords are rejected.\n\n**Columns available** on `news_article` (and as `select` / `group_by` items): `id`, `url`, `time_published`, `title`, `summary`, `source`, `source_domain`, `category`, `authors`, `topics`, `banner_image`, `overall_sentiment_score`, `overall_sentiment_label`, `tickers` (text[]), `ticker_data` (jsonb), `created_at`. Plus the synthetic `tk` alias — when referenced, the server auto-joins `UNNEST(tickers) AS tk`.\n\n**Aggregates**: `COUNT`, `AVG`, `SUM`, `MIN`, `MAX`, `STRING_AGG`.\n**Scalar funcs**: `COALESCE`, `NULLIF`, `ROUND`, `GREATEST`, `LEAST`, `TANH`, `ABS`, `DATE_TRUNC`, `EXTRACT`, `NOW`, `LOWER`, `UPPER`, `LENGTH`, `JSONB_ARRAY_LENGTH`.\n\nPagination is opaque cursor — pass `next_cursor` back as `?cursor=…`. For aggregate queries, alias every `group_by` expression so the cursor can resolve the next page (e.g. `group_by=time_published::date AS day`).",
    "input_schema": {
      "type": "object",
      "properties": {
        "q": {
          "type": "string",
          "description": "WHERE clause. Required. 4000-char max."
        },
        "select": {
          "type": "string",
          "description": "Comma-separated SELECT items. Default for article queries: `id, time_published, title, source, tickers, overall_sentiment_score, overall_sentiment_label`. Default for aggregate queries: the group_by keys + `COUNT(*) AS volume`."
        },
        "group_by": {
          "type": "string",
          "description": "Comma-separated grouping expressions. Switches the response shape to aggregate rows. Alias each expression for stable cursor pagination."
        },
        "having": {
          "type": "string",
          "description": "HAVING clause, applied post-aggregation. Requires `group_by`."
        },
        "order": {
          "type": "string",
          "description": "Column or alias to sort by. Defaults to `time_published` (article rows) or `volume` (aggregate rows)."
        },
        "dir": {
          "type": "string",
          "description": "`asc` or `desc`. Default `desc`."
        },
        "limit": {
          "type": "integer",
          "description": "Rows per page. Per-plan max: Pro 200, Scale/Enterprise 1000. Default 50."
        },
        "cursor": {
          "type": "string",
          "description": "Opaque pagination cursor from a previous response."
        }
      },
      "required": [
        "q"
      ]
    }
  }
]

Per-tool reference

One block per tool, indexed by name.

tickerbot_list_tickers

GET /v2/tickers

{
  "name": "tickerbot_list_tickers",
  "description": "GET /v2/tickers. List active tickers, paginated. Returns the universe of tracked symbols (~12,000 US equities + top 100 crypto by market cap) in alphabetical order with a slim payload (`ticker`, `name`, `asset_type`, `exchange`, `market_cap`). Use cursor pagination to walk the full list, or pass `?tickers=...` for bulk lookup of named symbols with full rows.",
  "input_schema": {
    "type": "object",
    "properties": {
      "tickers": {
        "type": "string",
        "description": "Comma-separated list of symbols (up to 50). When set, the response returns the full row for each requested symbol; pagination params are ignored. Symbols missing from our universe come back with `_not_found: true`."
      },
      "limit": {
        "type": "integer",
        "description": "Page size for the alphabetical walk. Max 1000.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from the previous response's `next_cursor` field. Continues the walk from after that page."
      },
      "search": {
        "type": "string",
        "description": "Substring filter over `ticker` (case-insensitive) and `name`. When set, results are ordered by `market_cap DESC NULLS LAST`."
      },
      "asset_type": {
        "type": "string",
        "description": "Filter by asset type — typically `equity` or `crypto`. Matched case-insensitively against the stored value."
      },
      "exchange": {
        "type": "string",
        "description": "Filter by exchange code (uppercase). Common values: `XNYS`, `XNAS`, `BATS`."
      },
      "sector": {
        "type": "string",
        "description": "Filter by sector name. Exact match against the stored `sector` field."
      },
      "min_market_cap": {
        "type": "number",
        "description": "Minimum market cap (USD). Results are ordered by `market_cap DESC NULLS LAST` when set."
      }
    }
  }
}

tickerbot_get_ticker

GET /v2/tickers/{ticker}

{
  "name": "tickerbot_get_ticker",
  "description": "GET /v2/tickers/{ticker}. Get the full current state of one ticker. Returns the entire ticker row. Every field on the schema page, including current values for all numeric signals and the current value of every boolean flag. Treat it as the authoritative current snapshot for a symbol.",
  "input_schema": {
    "type": "object",
    "properties": {
      "ticker": {
        "type": "string",
        "description": "Ticker symbol. Case-insensitive. Equities: exchange symbol (`AAPL`). Crypto: bare symbol (`BTC`)."
      }
    },
    "required": [
      "ticker"
    ]
  }
}

tickerbot_get_ticker_history

GET /v2/tickers/{ticker}/history

{
  "name": "tickerbot_get_ticker_history",
  "description": "GET /v2/tickers/{ticker}/history. Get the full ticker row as of a past date. Time-travel to any historical date and pull the whole wide row as it stood then, including the boolean flag values, technical indicators, and the most-recent fundamentals known on that date. Useful for backtests and \"what did we know\" reconstructions.",
  "input_schema": {
    "type": "object",
    "properties": {
      "ticker": {
        "type": "string",
        "description": "Ticker symbol."
      },
      "asof": {
        "type": "string",
        "description": "Target date as `YYYY-MM-DD` or full ISO timestamp `YYYY-MM-DDTHH:MM:SSZ`. The response returns the most-recent daily snapshot on or before this date."
      }
    },
    "required": [
      "ticker",
      "asof"
    ]
  }
}

tickerbot_get_ticker_events

GET /v2/tickers/{ticker}/events

{
  "name": "tickerbot_get_ticker_events",
  "description": "GET /v2/tickers/{ticker}/events. Discrete event log for one ticker. Returns splits, dividends, and analyst rating changes for the ticker, newest-first, in a unified envelope. Each event has a `ts`, a `kind`, and a `payload` whose shape depends on the kind.",
  "input_schema": {
    "type": "object",
    "properties": {
      "ticker": {
        "type": "string",
        "description": "Ticker symbol."
      },
      "from": {
        "type": "string",
        "description": "Earliest event timestamp (inclusive). YYYY-MM-DD or ISO. Defaults to your plan's history window."
      },
      "to": {
        "type": "string",
        "description": "Latest event timestamp (inclusive). YYYY-MM-DD or ISO. Defaults to \"now\"."
      },
      "kind": {
        "type": "string",
        "description": "Filter by event kind.",
        "enum": [
          "split",
          "dividend",
          "rating_change"
        ]
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 1000.",
        "default": 100
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from the previous response."
      }
    },
    "required": [
      "ticker"
    ]
  }
}

tickerbot_list_signals_catalog

GET /v2/signals

{
  "name": "tickerbot_list_signals_catalog",
  "description": "GET /v2/signals. Built-in signals plus your custom signals, each tagged with `kind`. Returns the unified signal catalog: every built-in column on the live schema (tagged `kind: 'builtin'`, carrying `name`, `type`, `source`) plus the caller's own custom signals (tagged `kind: 'expression'`, carrying `name`, `description`, `expr`, `created_at`, `updated_at`).\n\nPagination applies only to the custom-signal slice — built-ins are returned in their entirety on every page since they're a small fixed set.",
  "input_schema": {
    "type": "object",
    "properties": {
      "kind": {
        "type": "string",
        "description": "Filter by kind. Omit to return both.",
        "enum": [
          "builtin",
          "custom"
        ]
      },
      "limit": {
        "type": "integer",
        "description": "Page size for the custom-signal slice. Max 200.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from a prior response."
      }
    }
  }
}

tickerbot_get_signals_match

GET /v2/signals/{signal}

{
  "name": "tickerbot_get_signals_match",
  "description": "GET /v2/signals/{signal}. Find tickers that match a signal right now. Return the set of tickers where the named signal is true (for boolean flags) or where the numeric value satisfies a single-bounded condition. Sorted by signal value descending for numerics, alphabetically by ticker for booleans.",
  "input_schema": {
    "type": "object",
    "properties": {
      "signal": {
        "type": "string",
        "description": "A column on `ticker`. Boolean flags (e.g. `golden_cross_today`, `above_sma_50`) are detected automatically; numerics (e.g. `rsi_14`, `market_cap`, `pe_ratio`) require a condition."
      },
      "condition": {
        "type": "string",
        "description": "Required for numeric signals. Single bound, format `<op><value>`. Operators: `>`, `>=`, `=`, `!=`, `<`, `<=`. Examples: `>70`, `<=200`, `!=0`. Ignored for boolean flags."
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 200.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from the previous response."
      }
    },
    "required": [
      "signal"
    ]
  }
}

tickerbot_get_signal_history

GET /v2/signals/{signal}/{ticker}/history/{interval}

{
  "name": "tickerbot_get_signal_history",
  "description": "GET /v2/signals/{signal}/{ticker}/history/{interval}. Time series of one signal for one ticker. Returns a sequence of `{t, v}` bars for the requested signal × ticker at the chosen resolution. `t` is an ISO timestamp (or `YYYY-MM-DD` for daily/weekly). `v` is the signal value at that bar (a number for numeric signals, `true`/`false` for boolean flags).",
  "input_schema": {
    "type": "object",
    "properties": {
      "signal": {
        "type": "string",
        "description": "A column on the wide-state tables (matches the schema page 1:1)."
      },
      "ticker": {
        "type": "string",
        "description": "Ticker symbol."
      },
      "interval": {
        "type": "string",
        "description": "Resolution. Daily-only signals (SMAs, RSI, MACD, fundamentals) are not stored at minute/hourly resolution; request `1d` for those.",
        "enum": [
          "1m",
          "1h",
          "1d",
          "1w"
        ]
      },
      "from": {
        "type": "string",
        "description": "Earliest bar timestamp (inclusive). YYYY-MM-DD or ISO. Defaults to your plan's history window."
      },
      "to": {
        "type": "string",
        "description": "Latest bar timestamp (inclusive). YYYY-MM-DD or ISO. Defaults to \"now\"."
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 1000.",
        "default": 252
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor. Walks back in time page-by-page from the most-recent bar."
      }
    },
    "required": [
      "signal",
      "ticker",
      "interval"
    ]
  }
}

tickerbot_create_custom_signal

POST /v2/signals

{
  "name": "tickerbot_create_custom_signal",
  "description": "POST /v2/signals. Save a named SQL expression you can reference like a built-in signal. Persists a named boolean (or numeric) expression in the existing predicate grammar. Once saved, the name is referenceable from any SQL context — rule `q`, strategy `entry`, exit `signal_off`/`signal_on`, leg `what.scan` — and the predicate compiler inlines its body before SQL emit.\n\nThe expression is compiled at create time against the live signal column whitelist; references to other custom signals you own are inlined first (recursion is detected). Names are scoped per-user: `(user_id, name)` is the key. Names cannot collide with built-in column names — those return 409 `name_collision`.",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Slug — `^[a-z][a-z0-9_]{0,63}$`. Must not collide with any built-in column name."
      },
      "expr": {
        "type": "string",
        "description": "SQL expression in the predicate grammar. May reference built-in columns and other custom signals you own. Max 4000 chars."
      },
      "description": {
        "type": "string",
        "description": "Free-form notes. Max 500 chars."
      }
    },
    "required": [
      "name",
      "expr"
    ]
  }
}

tickerbot_patch_custom_signal

PATCH /v2/signals/{name}

{
  "name": "tickerbot_patch_custom_signal",
  "description": "PATCH /v2/signals/{name}. Edit the expression or description of one of your custom signals. Partial update — supply `expr`, `description`, or both. Providing `expr` recompiles against the live column whitelist (same validator as create). Built-in signals are read-only — only custom signals you own can be patched.",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Custom signal slug."
      },
      "expr": {
        "type": "string",
        "description": "New SQL expression. Re-validated and re-inlined against your other custom signals."
      },
      "description": {
        "type": "string",
        "description": "New description."
      }
    },
    "required": [
      "name"
    ]
  }
}

tickerbot_delete_custom_signal

DELETE /v2/signals/{name}

{
  "name": "tickerbot_delete_custom_signal",
  "description": "DELETE /v2/signals/{name}. Remove a custom signal — refused by default if anything references it. By default, DELETE is cascade-safe: it refuses with 409 `signal_referenced` if any of your strategies (or other custom signals) reference this signal by name. The response carries `referencing_strategies` and `referencing_signals` arrays so you can audit before deciding.\n\nPass `?force=true` to delete anyway. Existing references will compile to `unknown_column` errors next time the consumer recompiles, so use force when you've already fixed the references.",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Custom signal slug."
      },
      "force": {
        "type": "boolean",
        "description": "When `true`, skip the reference check and delete. References will break on next recompile.",
        "default": "false"
      }
    },
    "required": [
      "name"
    ]
  }
}

tickerbot_get_scan

GET /v2/scan

{
  "name": "tickerbot_get_scan",
  "description": "GET /v2/scan. Run a SQL WHERE against the current universe. Filter the live ticker universe with a SQL WHERE clause expressed in our SQL grammar. Returns matching tickers sorted by your chosen column. Columns in `q`, `order`, and `fields` are the customer-facing names listed on /api/schema and /api/flags. No translation layer.",
  "input_schema": {
    "type": "object",
    "properties": {
      "q": {
        "type": "string",
        "description": "SQL WHERE expression. Required unless `rule` is supplied. Max 4000 chars. Semicolons, comments, and write-side keywords (INSERT/UPDATE/DELETE/DROP/etc.) are rejected."
      },
      "rule": {
        "type": "string",
        "description": "Slug of a saved rule (see `/v2/rules`). Its `q`, `universe_id`, `order`, `dir`, and `fields` become defaults; any of those passed as query-string parameters override."
      },
      "universe": {
        "type": "string",
        "description": "Slug of a system universe (`top_10`, `top_100`) or one of your own (`/v2/universes`). Scopes the scan to those tickers. When omitted, the scan runs across all ~12,000 tracked tickers."
      },
      "asof": {
        "type": "string",
        "description": "Optional. Run the WHERE against historical daily state for the given `YYYY-MM-DD` (or full ISO). Plan history depth applies."
      },
      "order": {
        "type": "string",
        "description": "Column to sort by. Must be a valid identifier (lowercase letters, digits, underscore).",
        "default": "day_change_pct"
      },
      "dir": {
        "type": "string",
        "description": "Sort direction.",
        "enum": [
          "asc",
          "desc"
        ],
        "default": "desc"
      },
      "fields": {
        "type": "string",
        "description": "Comma-separated list of extra columns to include in each result row. Default columns are always present: ticker, name, asset_type, price, day_change_pct, gap_pct, relative_volume, market_cap."
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 100.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from the previous response."
      }
    }
  }
}

tickerbot_post_scan

POST /v2/scan

{
  "name": "tickerbot_post_scan",
  "description": "POST /v2/scan. Same as GET /v2/scan, with `q` in the body. POST mirror of `GET /v2/scan` for queries too long to fit in a URL. Accepts the same parameters (`q`, `order`, `dir`, `fields`, `limit`) in a JSON body. `cursor` continues to come from the query string for consistency with the GET pagination shape.",
  "input_schema": {
    "type": "object",
    "properties": {
      "q": {
        "type": "string",
        "description": "SQL WHERE expression."
      },
      "order": {
        "type": "string",
        "description": "Sort column."
      },
      "dir": {
        "type": "string",
        "description": "Sort direction.",
        "enum": [
          "asc",
          "desc"
        ]
      },
      "fields": {
        "type": "string",
        "description": "Comma-separated extra columns."
      },
      "limit": {
        "type": "integer",
        "description": "Page size, max 100."
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor (query string, not body)."
      }
    },
    "required": [
      "q"
    ]
  }
}

tickerbot_post_webhook

POST /v2/webhooks

{
  "name": "tickerbot_post_webhook",
  "description": "POST /v2/webhooks. Create a webhook subscription. Register a SQL WHERE clause (inline `q` or `rule_id`) + an HTTPS target URL. On the cadence configured for your plan, we evaluate the clause against the relevant ticker universe. When a new ticker enters the match set, we POST a `webhook.fired` event to your target. On creation we also send a one-shot `webhook.ping` to verify reachability; the subscription stays in `pending_verification` until the ping returns a 2xx response, then flips to `active`.",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Human-readable label. Helps you find this webhook later in `GET /v2/webhooks`. Doesn't need to be unique."
      },
      "q": {
        "type": "string",
        "description": "SQL WHERE clause. Same grammar as `/v2/scan`. Either this or `rule_id` is required."
      },
      "rule_id": {
        "type": "string",
        "description": "Slug of a saved rule (see `/v2/rules`). The webhook stores a snapshot of the rule's `q` + `universe_id` at creation time."
      },
      "target_url": {
        "type": "string",
        "description": "HTTPS endpoint that will receive POSTed events. Must start with `https://`. Plain HTTP is rejected."
      },
      "cadence": {
        "type": "string",
        "description": "How often to evaluate this webhook. Defaults to your plan's fastest cadence; pick a slower one to conserve quota or smooth out noisy fires.",
        "enum": [
          "1m",
          "5m",
          "15m",
          "hourly",
          "nyse_open"
        ]
      }
    },
    "required": [
      "name",
      "target_url"
    ]
  }
}

tickerbot_list_webhooks

GET /v2/webhooks

{
  "name": "tickerbot_list_webhooks",
  "description": "GET /v2/webhooks. List webhook subscriptions on this API key. Returns webhook subscriptions created by the current API key, newest-first. Visibility is key-scoped: webhooks created with a different key on the same account are not returned here. Use `?status=` to filter and the `cursor` field to paginate.",
  "input_schema": {
    "type": "object",
    "properties": {
      "status": {
        "type": "string",
        "description": "Filter by status.",
        "enum": [
          "active",
          "pending_verification",
          "disabled"
        ]
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 100.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from the previous response."
      }
    }
  }
}

tickerbot_get_webhook

GET /v2/webhooks/{id}

{
  "name": "tickerbot_get_webhook",
  "description": "GET /v2/webhooks/{id}. Fetch one webhook subscription. Returns the current state of a webhook subscription. Visibility is key-scoped: webhooks created by another key (even on the same account) return 404.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Webhook id returned by `POST /v2/webhooks`."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_delete_webhook

DELETE /v2/webhooks/{id}

{
  "name": "tickerbot_delete_webhook",
  "description": "DELETE /v2/webhooks/{id}. Delete a webhook subscription. Hard delete. Any pending deliveries already enqueued may still fire (they capture the target URL + signing secret at queue time). To pause-rather-than-delete, just stop accepting deliveries at your target; after enough consecutive failures the subscription flips to `disabled`.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Webhook id."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_list_webhook_deliveries

GET /v2/webhooks/{id}/deliveries

{
  "name": "tickerbot_list_webhook_deliveries",
  "description": "GET /v2/webhooks/{id}/deliveries. Inspect recent deliveries for a webhook. Returns the most-recent deliveries (pings and fires) for a webhook, newest-first. Useful for diagnosing failures and verifying signatures. The full request body is not included; we keep them small.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Webhook id."
      },
      "status": {
        "type": "string",
        "description": "Filter by delivery status.",
        "enum": [
          "pending",
          "delivered",
          "permanent_failure"
        ]
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 100.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_enable_webhook

POST /v2/webhooks/{id}/enable

{
  "name": "tickerbot_enable_webhook",
  "description": "POST /v2/webhooks/{id}/enable. Re-enable a disabled or pending webhook. Resets a webhook back to `pending_verification`, clears its match-state cache (so the next eval treats every currently-matching ticker as new and fires a single `webhook.fired`), and re-enqueues the handshake ping. On 2xx the webhook flips to `active`.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Webhook id."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_test_fire_webhook

POST /v2/webhooks/{id}/test

{
  "name": "tickerbot_test_fire_webhook",
  "description": "POST /v2/webhooks/{id}/test. Send a one-off synthetic delivery for QA / smoke testing. Writes a pending `webhook.fired` delivery with `test: true` and an empty `matches` array. The scheduler picks it up on its next tick and POSTs the same headers + signature shape as a real firing — the only difference is the `test` flag in the body and on the delivery row. Useful for verifying your receiver is reachable, parses the payload correctly, and verifies the signature.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Webhook id."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_list_universes

GET /v2/universes

{
  "name": "tickerbot_list_universes",
  "description": "GET /v2/universes. List your universes. A universe is a named list of tickers you can reference from `/v2/scan?universe=`, `/v2/signals/{signal}?universe=`, `/v2/tickers?universe=`, and `/v2/webhooks`. System universes (`top_10` and `top_100`, the top-N most-actively-traded tickers, rebalanced monthly) are available to every account at `GET /v2/universes/system`.",
  "input_schema": {
    "type": "object",
    "properties": {
      "limit": {
        "type": "integer",
        "description": "Page size. Max 100.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from the previous response."
      }
    }
  }
}

tickerbot_create_universe

POST /v2/universes

{
  "name": "tickerbot_create_universe",
  "description": "POST /v2/universes. Create a universe. Create a named ticker list owned by the calling account. Body: `{ id?, name, description?, tickers }`. If `id` is omitted, a slug is generated.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Optional slug. Lowercase letters, digits, underscore. Must be unique within your account."
      },
      "name": {
        "type": "string",
        "description": "Human-readable label."
      },
      "description": {
        "type": "string",
        "description": "Free-form notes."
      },
      "tickers": {
        "type": "array",
        "description": "Ticker symbols. Validated against your plan scope + the active universe.",
        "items": {
          "type": "string"
        }
      }
    },
    "required": [
      "name",
      "tickers"
    ]
  }
}

tickerbot_list_system_universes

GET /v2/universes/system

{
  "name": "tickerbot_list_system_universes",
  "description": "GET /v2/universes/system. List system universes (top_10, top_100). System universes are read-only ticker lists rebalanced monthly by 30-day trailing dollar volume. Available to every account regardless of plan.",
  "input_schema": {
    "type": "object",
    "properties": {}
  }
}

tickerbot_get_universe

GET /v2/universes/{id}

{
  "name": "tickerbot_get_universe",
  "description": "GET /v2/universes/{id}. Fetch a single universe. Returns the universe doc. Use `top_10`/`top_100` to fetch a system universe; any other slug must be one your account owns.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Universe slug."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_patch_universe

PATCH /v2/universes/{id}

{
  "name": "tickerbot_patch_universe",
  "description": "PATCH /v2/universes/{id}. Update a universe. Update `name`, `description`, or `tickers`. To add/remove ticker subsets without replacing the full list, pass `{ add: [...], remove: [...] }` instead of `tickers`.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Universe slug."
      },
      "name": {
        "type": "string",
        "description": "New label."
      },
      "description": {
        "type": "string",
        "description": "New notes."
      },
      "tickers": {
        "type": "array",
        "description": "Replace the full ticker list.",
        "items": {
          "type": "string"
        }
      },
      "add": {
        "type": "array",
        "description": "Add these tickers (deduplicated).",
        "items": {
          "type": "string"
        }
      },
      "remove": {
        "type": "array",
        "description": "Remove these tickers.",
        "items": {
          "type": "string"
        }
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_delete_universe

DELETE /v2/universes/{id}

{
  "name": "tickerbot_delete_universe",
  "description": "DELETE /v2/universes/{id}. Delete a universe. Permanently delete one of your universes. System universes cannot be deleted.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Universe slug."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_list_rules

GET /v2/rules

{
  "name": "tickerbot_list_rules",
  "description": "GET /v2/rules. List your saved rules. A rule is a saved `{q, universe_id?, order, dir, fields}` bundle. Rules can be referenced from `/v2/scan?rule=` (one-off run) and `/v2/webhooks` (subscribe to fires). Same SQL grammar as `/v2/scan`.",
  "input_schema": {
    "type": "object",
    "properties": {
      "limit": {
        "type": "integer",
        "description": "Page size. Max 100.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor."
      }
    }
  }
}

tickerbot_create_rule

POST /v2/rules

{
  "name": "tickerbot_create_rule",
  "description": "POST /v2/rules. Create a saved rule. Save a scan bundle so it can be re-run with `/v2/scan?rule=` or subscribed to via `/v2/webhooks`. The `universe_id` is optional; if unset, the rule runs against your plan default at execution time.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Optional slug."
      },
      "name": {
        "type": "string",
        "description": "Human-readable label."
      },
      "description": {
        "type": "string",
        "description": "Free-form notes."
      },
      "q": {
        "type": "string",
        "description": "SQL WHERE clause."
      },
      "universe_id": {
        "type": "string",
        "description": "Slug of a system or caller-owned universe to scope this rule."
      },
      "order": {
        "type": "string",
        "description": "Sort column.",
        "default": "day_change_pct"
      },
      "dir": {
        "type": "string",
        "description": "Sort direction.",
        "enum": [
          "asc",
          "desc"
        ]
      },
      "fields": {
        "type": "array",
        "description": "Extra columns to include.",
        "items": {
          "type": "string"
        }
      }
    },
    "required": [
      "name",
      "q"
    ]
  }
}

tickerbot_get_rule

GET /v2/rules/{id}

{
  "name": "tickerbot_get_rule",
  "description": "GET /v2/rules/{id}. Fetch a saved rule. Returns the rule doc.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Rule slug."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_patch_rule

PATCH /v2/rules/{id}

{
  "name": "tickerbot_patch_rule",
  "description": "PATCH /v2/rules/{id}. Update a saved rule. Update any field on a rule. To detach `universe_id` set it to `null`.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Rule slug."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_delete_rule

DELETE /v2/rules/{id}

{
  "name": "tickerbot_delete_rule",
  "description": "DELETE /v2/rules/{id}. Delete a saved rule. Webhooks referencing this rule continue to fire using their snapshotted `q`; they don't break.",
  "input_schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "Rule slug."
      }
    },
    "required": [
      "id"
    ]
  }
}

tickerbot_create_strategy

POST /v2/strategies

{
  "name": "tickerbot_create_strategy",
  "description": "POST /v2/strategies. Create a multi-leg strategy. Persist a strategy under your account. The body carries five top-level fields: `name` (slug), `description` (free-form), `allocation` (the dollar pot legs size against), `cadence` (when the runner ticks the strategy), and `legs` (one or more rules — what to trade, when to enter, when to exit, how much).\n\nA leg's `what` is one of three forms: `{ ticker: \"AAPL\" }`, `{ universe: \"<slug>\" }`, or `{ scan: \"<SQL WHERE>\" }`. `entry` is either a bare SQL string (single-tranche, simple case) or an array of `{ condition, weight }` objects for scaling in. `exits` is an ordered, first-match-wins list of typed exits (10 types — see notes). `sizing` picks one of `fixed_fractional`, `risk_based`, or `vol_targeted`. `max_concurrent_positions` caps how many tickers the leg can hold at once (default 1).\n\nEvery embedded SQL expression — `what.scan`, `entry` conditions, `signal_off`/`signal_on` exits — is compiled at create time against the live signal column whitelist. Compile errors are returned as a 400 with the failing path. Created strategies start in `status: 'stopped'` — flip to `'running'` via PATCH when you're ready for the runner to tick it.",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Slug — `^[a-z][a-z0-9_]{0,63}$`, unique per account."
      },
      "description": {
        "type": "string",
        "description": "Free-form notes. Max 500 chars."
      },
      "allocation": {
        "type": "number",
        "description": "Dollar pot the legs size against. Positive number."
      },
      "cadence": {
        "type": "string",
        "description": "Either `{ type: \"recurring\", interval: \"1m\"|\"5m\"|\"15m\"|\"1h\"|\"1d\", at?: \"bar_close\"|\"open\"|\"close\"|\"premarket\" }` or `{ type: \"one_time\", after: <ISO timestamp or epoch> }`."
      },
      "legs": {
        "type": "string",
        "description": "One or more legs. Each leg: `{ name?, side: \"long\"|\"short\", what, entry, exits?, sizing, max_concurrent_positions? }`. See description and STRATEGY_DESIGN.md §4 for full shape."
      }
    },
    "required": [
      "name",
      "allocation",
      "cadence",
      "legs"
    ]
  }
}

tickerbot_get_strategy

GET /v2/strategies/{name}

{
  "name": "tickerbot_get_strategy",
  "description": "GET /v2/strategies/{name}. Fetch a saved strategy. Returns the full strategy definition for the calling user — same shape POST returns. Soft-deleted strategies are still accessible by name (their `status` field is `\"deleted\"`).",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Strategy slug."
      }
    },
    "required": [
      "name"
    ]
  }
}

tickerbot_list_strategies

GET /v2/strategies

{
  "name": "tickerbot_list_strategies",
  "description": "GET /v2/strategies. List your strategies, newest first. Cursor-paginated. Sort: `created_at` desc, then `name` asc. Soft-deleted strategies are excluded unless you pass `?status=deleted`.",
  "input_schema": {
    "type": "object",
    "properties": {
      "status": {
        "type": "string",
        "description": "Filter by status.",
        "enum": [
          "running",
          "stopped",
          "ended",
          "deleted"
        ]
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 200.",
        "default": 50
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from a prior response."
      }
    }
  }
}

tickerbot_patch_strategy

PATCH /v2/strategies/{name}

{
  "name": "tickerbot_patch_strategy",
  "description": "PATCH /v2/strategies/{name}. Partial update — change any subset of fields, including status. Any of `description`, `allocation`, `cadence`, `legs`, and `status` may be supplied; missing fields are unchanged. Providing `legs` recompiles every embedded SQL expression — same validator as POST. Use `status` to start or stop the runner: `'running'` makes the strategy eligible to tick, `'stopped'` pauses it (open positions persist and stay individually closeable). Use DELETE to soft-delete.",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Strategy slug (immutable)."
      },
      "description": {
        "type": "string",
        "description": "New description. Max 500 chars."
      },
      "allocation": {
        "type": "number",
        "description": "New dollar pot. Positive number."
      },
      "cadence": {
        "type": "string",
        "description": "Replace the cadence. Same shape as create."
      },
      "legs": {
        "type": "string",
        "description": "Replace the legs array. Every SQL expression is recompiled."
      },
      "status": {
        "type": "string",
        "description": "Set runner state.",
        "enum": [
          "running",
          "stopped"
        ]
      }
    },
    "required": [
      "name"
    ]
  }
}

tickerbot_delete_strategy

DELETE /v2/strategies/{name}

{
  "name": "tickerbot_delete_strategy",
  "description": "DELETE /v2/strategies/{name}. Soft-delete a strategy; optionally force-close open positions. By default, DELETE soft-deletes: the runner stops evaluating, but the strategy record and its positions remain queryable and individually closeable. This is the default because force-closing real positions on an accidental DELETE would be costly.\n\nPass `?force=true` to soft-delete and force-close every open position belonging to this strategy in one call. Closed positions get `exit_reason: 'force_close'` and `shares_remaining: 0`.",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Strategy slug."
      },
      "force": {
        "type": "boolean",
        "description": "When `true`, also force-close all open positions for this strategy.",
        "default": "false"
      }
    },
    "required": [
      "name"
    ]
  }
}

tickerbot_list_positions

GET /v2/strategies/{name}/positions

{
  "name": "tickerbot_list_positions",
  "description": "GET /v2/strategies/{name}/positions. Positions opened by the strategy's runner. Lists positions belonging to a strategy. The position table is Tickerbot's own ledger — it records what the strategy did, not what's actually held at a brokerage. Position rows carry `entry_price` (shares-weighted across filled tranches), `entry_atr`, `peak_price`, share counts, `tranches_fired`, `realized_pnl_dollars`, and `exit_reason` (set when closed).",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Strategy slug."
      },
      "status": {
        "type": "string",
        "description": "Filter by open/closed.",
        "enum": [
          "open",
          "closed",
          "all"
        ],
        "default": "open"
      },
      "limit": {
        "type": "integer",
        "description": "Page size. Max 500.",
        "default": 100
      }
    },
    "required": [
      "name"
    ]
  }
}

tickerbot_close_position

POST /v2/strategies/{name}/positions/{id}/close

{
  "name": "tickerbot_close_position",
  "description": "POST /v2/strategies/{name}/positions/{id}/close. Manually close (or partially close) an open position. Force-close a position at a caller-supplied price. The endpoint marks the position closed (or scales it down via `size_pct`), updates `shares_remaining` and `realized_pnl_dollars`, and sets `exit_reason: 'manual'` when the position is fully closed.\n\nPnL math: `(price - entry_price) * shares_to_close * direction` where `direction` is +1 for long, -1 for short. `price` is supplied by the caller because the v1 runner does not yet have a live-quote integration; the caller is expected to pass the current market price (or a fill price from an external broker).",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Strategy slug."
      },
      "id": {
        "type": "string",
        "description": "Position UUID — see the list-positions endpoint."
      },
      "price": {
        "type": "number",
        "description": "Close price. Positive number."
      },
      "size_pct": {
        "type": "number",
        "description": "Fraction of remaining shares to close, in (0, 100]. Default 100 = full close.",
        "default": 100
      }
    },
    "required": [
      "name",
      "id",
      "price"
    ]
  }
}

tickerbot_run_tick

POST /v2/strategies/{name}/run-tick

{
  "name": "tickerbot_run_tick",
  "description": "POST /v2/strategies/{name}/run-tick. Evaluate the strategy once against the latest bar. Runs a single tick of the live runner: evaluates exits on any open positions for this strategy at the latest bar (first-match-wins per position), then evaluates tranche entries on still-open positions, then opens new positions on candidates that pass entry and fit under `max_concurrent_positions`. Lifecycle changes are written to the position ledger.\n\nIntended for: (a) orchestration calls from Claude Code, (b) manual \"fire this strategy now\" UX, (c) a future scheduler. Auto-running on cadence is not yet wired — until it is, `run-tick` is how a strategy executes.",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Strategy slug."
      }
    },
    "required": [
      "name"
    ]
  }
}

tickerbot_strategy_backtest

GET /v2/strategies/{name}/backtest

{
  "name": "tickerbot_strategy_backtest",
  "description": "GET /v2/strategies/{name}/backtest. Walk historical bars and return trades + equity curve + summary. Runs the strategy through historical bars at its own configured cadence interval (the backtest does **not** take an `interval` override — backtesting at a cadence different from the one the strategy is configured for produces results that don't reflect how the strategy would actually run). Entries fire on false→true transitions; exits evaluate first each bar, first-match-wins. PnL is computed per fill with `cost_bps` applied to both sides.\n\nReturns `{ trades, equity_curve, summary }` per STRATEGY_DESIGN.md §7. The equity curve enables Sharpe + drawdown calculation and dashboard charts; the summary is what the dashboard's metric strip displays.\n\n**MVP scope.** The engine currently supports: single-leg strategies, single-ticker `what` only, `fixed_fractional` sizing only, single-string (non-tranched) entry, full-close exits (`size_pct = 100`), and exit types `stop_loss_pct`, `take_profit_pct`, `trailing_stop_pct`, `max_hold_bars`, `signal_off`, `signal_on`. Long is fully supported; short is recognised but not yet wired. Strategies outside this envelope get a 400 `not_supported` naming the first unsupported feature.",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Strategy slug."
      },
      "from": {
        "type": "string",
        "description": "Earliest bar (inclusive). ISO timestamp or epoch ms. Defaults to 90 days ago."
      },
      "to": {
        "type": "string",
        "description": "Latest bar (inclusive). ISO timestamp or epoch ms. Defaults to now."
      },
      "cost_bps": {
        "type": "number",
        "description": "Round-trip cost in basis points, applied to both sides of every fill. Non-negative.",
        "default": 5
      }
    },
    "required": [
      "name"
    ]
  }
}

tickerbot_list_news_scan

GET /v2/news/scan

{
  "name": "tickerbot_list_news_scan",
  "description": "GET /v2/news/scan. SQL-style scan of the news_article archive — article rows or aggregate rollups. One endpoint, two shapes. With no `group_by` you get article rows; with `group_by` you get rollups (counts, averages) grouped by your expressions. Auto-joins `UNNEST(tickers) AS tk` whenever any clause references the `tk` alias — that's how you write per-ticker rollups without unnesting yourself.\n\nThere is no separate \"asof\" parameter. Historical queries are just WHERE filters on `time_published`:\n\n```\nq=time_published >= '2026-01-15' AND time_published < '2026-01-16'\nq=time_published >= NOW() - INTERVAL '7 days'\n```\n\nThe grammar is the same SQL whitelist used by `/v2/scan`: comparison and logical operators, casts (`::numeric`, `::date`, …), array operators (`= ANY(tickers)`, `tickers && ARRAY[…]`), JSONB operators (`->`, `->>`, `@>`), and the scalar/aggregate functions listed below. Subqueries and DDL keywords are rejected.\n\n**Columns available** on `news_article` (and as `select` / `group_by` items): `id`, `url`, `time_published`, `title`, `summary`, `source`, `source_domain`, `category`, `authors`, `topics`, `banner_image`, `overall_sentiment_score`, `overall_sentiment_label`, `tickers` (text[]), `ticker_data` (jsonb), `created_at`. Plus the synthetic `tk` alias — when referenced, the server auto-joins `UNNEST(tickers) AS tk`.\n\n**Aggregates**: `COUNT`, `AVG`, `SUM`, `MIN`, `MAX`, `STRING_AGG`.\n**Scalar funcs**: `COALESCE`, `NULLIF`, `ROUND`, `GREATEST`, `LEAST`, `TANH`, `ABS`, `DATE_TRUNC`, `EXTRACT`, `NOW`, `LOWER`, `UPPER`, `LENGTH`, `JSONB_ARRAY_LENGTH`.\n\nPagination is opaque cursor — pass `next_cursor` back as `?cursor=…`. For aggregate queries, alias every `group_by` expression so the cursor can resolve the next page (e.g. `group_by=time_published::date AS day`).",
  "input_schema": {
    "type": "object",
    "properties": {
      "q": {
        "type": "string",
        "description": "WHERE clause. Required. 4000-char max."
      },
      "select": {
        "type": "string",
        "description": "Comma-separated SELECT items. Default for article queries: `id, time_published, title, source, tickers, overall_sentiment_score, overall_sentiment_label`. Default for aggregate queries: the group_by keys + `COUNT(*) AS volume`."
      },
      "group_by": {
        "type": "string",
        "description": "Comma-separated grouping expressions. Switches the response shape to aggregate rows. Alias each expression for stable cursor pagination."
      },
      "having": {
        "type": "string",
        "description": "HAVING clause, applied post-aggregation. Requires `group_by`."
      },
      "order": {
        "type": "string",
        "description": "Column or alias to sort by. Defaults to `time_published` (article rows) or `volume` (aggregate rows)."
      },
      "dir": {
        "type": "string",
        "description": "`asc` or `desc`. Default `desc`."
      },
      "limit": {
        "type": "integer",
        "description": "Rows per page. Per-plan max: Pro 200, Scale/Enterprise 1000. Default 50."
      },
      "cursor": {
        "type": "string",
        "description": "Opaque pagination cursor from a previous response."
      }
    },
    "required": [
      "q"
    ]
  }
}

How to wire it up

The runtime calls the tool. Your code forwards to api.tickerbot.io.

When the LLM decides to call a tool, your code receives the tool name and arguments. Route the call to the matching Tickerbot endpoint (the endpoint field on each tool body above, or hard-coded in your handler), forward the bearer token, and return the JSON response to the LLM.

The OpenAPI spec covers anything generator-driven if your runtime isn't one of the three above.