# Programmable Queries #### NOTE Programmable queries currently apply to a **single tool: symbol universe** (`search_symbol_universe`). The `search_symbol_universe` tool runs a OneTick query to return instruments and their attributes. By default it runs a **bundled OneTick Cloud query** (`onetick_llmbase.mcp.default_queries.symbol_universe_user_query`) against the `SYMBOL_UNIVERSE` database. You can replace that query with your own — pointing at your own database, tick type, columns, and filters — without forking the server, by setting the `OTP_SYMBOL_UNIVERSE_QUERY_PATH` environment variable to a Python file that follows the contract below. ## What your query controls — and what the server handles Your query file is responsible for **one thing**: describing *which instruments/rows to return and how to filter them*. It returns an `onetick.py` source object. Everything else is handled by the server around your query, so your file stays small and you do **not** implement it: | Handled by the server | Notes | |-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Date range | Derived from the `start_date` / `end_date` tool arguments the model supplies at calltime (see `Date range and query span`). Your query does **not** set the time window. | | Result limit / size | The result count is capped (see `Result size`). | | Authentication | OneTick WebAPI / auth modes (`OTP_AUTH_MODE`). See `OneTick WebAPI authentication`. | | Execution & timeout | The server runs your source with `otp.run(...)` under `OTP_QUERY_TIMEOUT`. | | Formatting | Rows are serialized into the tool result returned to the model. | Because the date window is applied by the server at execution time (it is an argument to `otp.run`, not a filter inside the query graph), it is intentionally **not** part of your query file. See `Date range and query span` for how to widen it if your data is not a daily snapshot. ## Query module contract A query file must define two names: `SUPPORTED_FILTERS` *(dict)* : A `{filter_name: description}` mapping. The descriptions are included in the tool description so the model knows which filters are available and what they mean — write them for the model to read. At runtime, only filter names listed here that also carry a value are passed to `build_query`; unknown names and empty values are dropped. `build_query(otp, settings, filters)` *(callable)* : Receives: - `otp` — the `onetick.py` module. - `settings` — the server settings object (read your own configuration values here, e.g. any custom fields you added to a `Settings` subclass). - `filters` — a `dict` of the validated filters actually supplied for this call (a subset of `SUPPORTED_FILTERS`). It must return an `onetick.py` source object that `otp.run()` can execute. ## Example ```python # my_query.py SUPPORTED_FILTERS = { "exchange": "Filter by Market Identifier Code (e.g. XNAS, XNYS).", "currency": "Filter by trading currency (e.g. USD, EUR, GBP).", } def build_query(otp, settings, filters): data = otp.DataSource( db="MY_UNIVERSE_DB", tick_type="SYMBOLS", schema_policy="manual", ) if "exchange" in filters: data = data.where(otp.Column("MIC", str) == filters["exchange"]) if "currency" in filters: data = data.where(otp.Column("CURRENCY", str) == filters["currency"]) return data ``` Point the server at it: ``` OTP_SYMBOL_UNIVERSE_QUERY_PATH=/path/to/my_query.py ``` For a fuller, production example — including selecting the symbol set with `otp.Symbols` and merging across symbols — see the bundled default module described at the top of this page. ## Enabling the tool Programmable queries only apply when the Query Based tool group is enabled and OneTick access is configured: - Include `queries` in `LLMBASE_MCP_TOOLS` (or leave the default, which enables all tool groups). See `Tools`. - Configure OneTick authentication (`OTP_AUTH_MODE` and its credentials, or `none` for an unauthenticated gateway). See `OneTick WebAPI authentication`. ## Date range and query span The model chooses the date window per call through the tool’s `start_date` / `end_date` arguments: - If neither is given, the window defaults to the **previous trading day** (a one-day span). - If only `start_date` is given, `end_date` defaults to `start_date + 1 day`. The effective span is then **capped** to `MAX_QUERY_SPAN_DAYS` (default `1`). The bundled `SYMBOL_UNIVERSE` query is a point-in-time **daily snapshot**, so a wider range is never more useful than a single day — it only multiplies the per-day scan and can time out. Over-wide, inverted, or empty ranges are clamped to this span. #### IMPORTANT If your custom query is **not** a daily snapshot and genuinely needs multi-day ranges (for example, “instruments seen over the last 5 days”), raise `MAX_QUERY_SPAN_DAYS` — otherwise requests for a wider window are silently narrowed. Set a large value to effectively disable the cap. | Variable | Default | Description | |----------------------------------|-----------|------------------------------------------------------------------------------------------------------------------------------| | `OTP_SYMBOL_UNIVERSE_QUERY_PATH` | *(unset)* | Path to your query file. Unset → the bundled default query runs. | | `MAX_QUERY_SPAN_DAYS` | `1` | Maximum `[start, end]` span (in days) the tool will query. Raise it for non-snapshotqueries that need multi-day ranges. | | `OTP_QUERY_TIMEOUT` | `120` | Seconds before a running query is aborted with a clear error. | See `Configuration` for the full variable reference. ## Result size The number of rows returned is clamped to an internal maximum (50), regardless of the `limit` the model requests. Wide symbol rows would otherwise exceed the MCP client’s tool-result size limit. When results are truncated, the tool tells the model to **narrow the query with filters** rather than to request more rows. Design `SUPPORTED_FILTERS` so the model can meaningfully narrow broad requests. ## Timeout Each query runs under `OTP_QUERY_TIMEOUT` (default `120` seconds). A query that exceeds it is aborted and returns a clear error message instead of blocking the server. Keep custom queries selective (push filters into the query, avoid full scans) so they stay well within the timeout. ## AST guardrails Operator-supplied query files are scanned with an AST allow-list before loading. The scan is a guardrail against accidents, not a security sandbox — the file is treated as operator-owned config-as-code (the same trust level as your `.env` or the wheel itself). The bundled default query is trusted and is **not** scanned. Allowed import roots: `onetick`, `datetime`, `typing`, `math`. Banned calls: `open`, `exec`, `eval`, `compile`, `__import__`, `input`, `breakpoint`, and any dunder attribute access. ## Startup validation The query module is loaded and validated **once at server startup**, so mistakes surface immediately rather than on the first query. The server fails fast with a clear error if the file: - does not exist at `OTP_SYMBOL_UNIVERSE_QUERY_PATH`, - has a syntax error or fails the AST scan, - is missing `SUPPORTED_FILTERS` (must be a `dict`) or `build_query` (must be callable).