{
  "openapi": "3.1.0",
  "info": {
    "title": "InvestDX Data API",
    "description": "Read-only API keys. Money is serialized as strings. Daily: closed WIB dates. Hourly: settled bars through today; forming bars excluded, five-minute settlement delay. 2h/4h are complete, session-anchored 1h aggregates; short sessions are labeled with duration_minutes/session_minutes. Daily is never derived from hourly. Technical snapshots use 200 observed bars. Volume is expressed in shares. Omitting start/end returns the full stored history via cursor paging; explicit date windows are capped at 366 calendar days. Arrays use data for symbols/candles/screener; rows for foreign-flow/broker-summary. Research data is an object plus chart array; datasets is a keyed object. Quotes/correlations are retired (410); fundamentals is not offered (404), with no equivalent replacement.",
    "version": "1.6.0"
  },
  "paths": {
    "/v1/symbols": {
      "get": {
        "tags": [
          "Symbols"
        ],
        "summary": "Symbols",
        "operationId": "symbols_v1_symbols_get",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 64,
              "default": "",
              "title": "Q"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 500,
              "minimum": 1,
              "default": 100,
              "title": "Limit"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 100000,
              "minimum": 0,
              "default": 0,
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "title": "Response Symbols V1 Symbols Get",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CatalogSymbol"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        },
        "description": " Static catalog metadata only: lot_size is shares per lot from the committed CSV, null when absent; listing_date is its recorded ISO date, null for missing/1900-01-01 placeholders. Neither is a verified current or point-in-time exchange eligibility feed. Tick size explicitly declined: no maintained authoritative price/board/date-aware tick schedule; legacy retired strategy ladders are not a verified exchange source. Delisting dates explicitly declined: no source. Trading-status/suspension flags explicitly declined: no authoritative current/historical feed; zero volume/missing bars do not prove suspension. Never infer active status from catalog membership or ready indicators. Suspension-aware signal exclusion and reliable pre-listing forward-test eligibility are not supplied: obtain independent authoritative point-in-time status/listing data first."
      }
    },
    "/v1/candles/{symbol}": {
      "get": {
        "tags": [
          "Candles"
        ],
        "summary": "Candles",
        "operationId": "candles_v1_candles__symbol__get",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "symbol",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Z]{4}$",
              "title": "Symbol"
            }
          },
          {
            "name": "interval",
            "in": "query",
            "required": false,
            "schema": {
              "enum": [
                "1d",
                "1h",
                "2h",
                "4h",
                "1w",
                "1mo"
              ],
              "type": "string",
              "default": "1d",
              "title": "Interval"
            }
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "date"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Start"
            }
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "date"
                },
                {
                  "type": "null"
                }
              ],
              "title": "End"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 2000,
              "minimum": 1,
              "default": 500,
              "title": "Limit"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 100000,
              "minimum": 0,
              "default": 0,
              "title": "Cursor"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Bar order. asc (default) returns oldest first; desc returns newest first.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          },
          {
            "name": "indicators",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 256
                },
                {
                  "type": "null"
                }
              ],
              "description": "all or comma-separated: change_pct, vol_sma_5, vol_sma_20, sma_5, sma_8, sma_21, sma_60, sma_100, sma_200, sma_400, sma_800, rsi_14, atr_14, macd, macd_signal, macd_histogram, stoch_k, stoch_d",
              "title": "Indicators"
            },
            "description": "all or comma-separated: change_pct, vol_sma_5, vol_sma_20, sma_5, sma_8, sma_21, sma_60, sma_100, sma_200, sma_400, sma_800, rsi_14, atr_14, macd, macd_signal, macd_histogram, stoch_k, stoch_d"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CandlePage"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "429": {
            "description": "Shared account RPS"
          },
          "503": {
            "description": "Publication on hold or data unavailable"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        },
        "description": " 1w/1mo derive from canonical daily candles: Monday-start WIB weeks and calendar months. Only whole, closed periods with all expected trading-day inputs are returned; labels use period-start midnight, including non-trading dates. Partial date-window buckets are omitted. Successful reads may be cached for up to 60 seconds; authentication and RPS admission still apply. Omit start/end for full stored history paged by cursor; cursor indexes ascending bars, beyond-window cursors return 422 cursor_beyond_window. With start or end, the window is capped at 366 calendar days. Unknown or repeated query parameters return 422. Derived bars expose duration_minutes (label to end) and session_minutes (scheduled source session time inside the bucket, including the closing phase). Every /v1/candles row includes settled_at (RFC3339 WIB scheduled read eligibility) and is_final (eligibility at response as_of); closed_bars_only excludes rows before settled_at, so returned rows have is_final=true. This is not an ingestion timestamp or an immutability guarantee: late upstream corrections can change closed rows. Use settled_at, not bucket_start + interval or closed_before, to interpret eligibility. Intraday eligibility is the last source hour's close plus five minutes; the 16:00 closing bar is eligible at 16:20 WIB. A Thursday 2h bucket labeled 15:00-17:00 therefore settles at 16:20, not 17:05. Daily settles at next WIB midnight; weekly and monthly at the next period boundary. Incomplete groups remain omitted. No calendar/session endpoint is provided: per-row metadata avoids a client holiday table for returned rows, but does not predict missing/future sessions. Calendar years outside the maintained holiday table use the existing weekday approximation. Cached as_of remains the original evaluation time; corrections may lag up to 60 seconds."
      }
    },
    "/v1/technicals/{symbol}": {
      "get": {
        "tags": [
          "Technicals"
        ],
        "summary": "Technicals",
        "operationId": "technicals_v1_technicals__symbol__get",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "symbol",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Z]{4}$",
              "title": "Symbol"
            }
          },
          {
            "name": "interval",
            "in": "query",
            "required": false,
            "schema": {
              "enum": [
                "1d",
                "1h",
                "2h",
                "4h",
                "1w",
                "1mo"
              ],
              "type": "string",
              "default": "1d",
              "title": "Interval"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "title": "Response Technicals V1 Technicals  Symbol  Get",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "missing_bars": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "description": "missing_bars is a sorted array of expected-but-absent bar labels (RFC3339 WIB), computed from the SAME calendar as missing_expected_bars; its length equals the count for evaluated histories. Daily entries are midnight dates, intraday entries are bucket labels, weekly/monthly entries are period starts. Scope starts at input_first in the observed seed and ends at closed_before; not a lifetime gap inventory. Empty/no_data or invalid_data histories return [] without proving completeness. Known holidays and non-session slots are excluded; calendar_covered_through_year is 2026, with the existing weekday approximation outside maintained holiday coverage. A missing expected bar is not proof of ingestion failure, pre-listing eligibility, suspension or tradability."
                        },
                        "input_first": {
                          "anyOf": [
                            {
                              "type": "string",
                              "format": "date-time"
                            },
                            {
                              "type": "null"
                            }
                          ],
                          "description": "First observed bar in the evaluated seed; null without history."
                        },
                        "calendar_covered_through_year": {
                          "type": "integer",
                          "description": "Current maintained holiday coverage year; not complete historical listing/status coverage."
                        }
                      },
                      "required": [
                        "missing_bars",
                        "input_first",
                        "calendar_covered_through_year"
                      ]
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        },
        "description": " missing_bars is a sorted array of expected-but-absent bar labels (RFC3339 WIB), computed from the SAME calendar as missing_expected_bars; its length equals the count for evaluated histories. Daily entries are midnight dates, intraday entries are bucket labels, weekly/monthly entries are period starts. Scope starts at input_first in the observed seed and ends at closed_before; not a lifetime gap inventory. Empty/no_data or invalid_data histories return [] without proving completeness. Known holidays and non-session slots are excluded; calendar_covered_through_year is 2026, with the existing weekday approximation outside maintained holiday coverage. A missing expected bar is not proof of ingestion failure, pre-listing eligibility, suspension or tradability."
      }
    },
    "/v1/screener": {
      "get": {
        "tags": [
          "Screener"
        ],
        "summary": "Screener",
        "operationId": "screener_v1_screener_get",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "interval",
            "in": "query",
            "required": false,
            "schema": {
              "enum": [
                "1d",
                "1h",
                "2h",
                "4h",
                "1w",
                "1mo"
              ],
              "type": "string",
              "default": "1d",
              "title": "Interval"
            }
          },
          {
            "name": "signal",
            "in": "query",
            "required": false,
            "schema": {
              "enum": [
                "all",
                "rsi_oversold",
                "rsi_overbought",
                "macd_bullish_cross",
                "macd_bearish_cross",
                "stoch_bullish_cross",
                "stoch_bearish_cross"
              ],
              "type": "string",
              "default": "all",
              "title": "Signal"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 25,
              "minimum": 1,
              "default": 25,
              "title": "Limit"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 100000,
              "minimum": 0,
              "default": 0,
              "title": "Cursor"
            }
          },
          {
            "name": "scan_id",
            "in": "query",
            "required": false,
            "description": "Scan identifier returned by page 0. Required with cursor>0; a missing or expired scan returns 409.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "title": "Response Screener V1 Screener Get",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "missing_bars": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "description": "missing_bars is a sorted array of expected-but-absent bar labels (RFC3339 WIB), computed from the SAME calendar as missing_expected_bars; its length equals the count for evaluated histories. Daily entries are midnight dates, intraday entries are bucket labels, weekly/monthly entries are period starts. Scope starts at input_first in the observed seed and ends at closed_before; not a lifetime gap inventory. Empty/no_data or invalid_data histories return [] without proving completeness. Known holidays and non-session slots are excluded; calendar_covered_through_year is 2026, with the existing weekday approximation outside maintained holiday coverage. A missing expected bar is not proof of ingestion failure, pre-listing eligibility, suspension or tradability."
                          },
                          "input_first": {
                            "anyOf": [
                              {
                                "type": "string",
                                "format": "date-time"
                              },
                              {
                                "type": "null"
                              }
                            ],
                            "description": "First observed bar in the evaluated seed; null without history."
                          },
                          "calendar_covered_through_year": {
                            "type": "integer",
                            "description": "Current maintained holiday coverage year; not complete historical listing/status coverage."
                          },
                          "sector": {
                            "type": "string",
                            "description": "Committed catalog label, same as /v1/symbols; may be empty/Unknown, not point-in-time metadata."
                          },
                          "board": {
                            "type": "string",
                            "description": "Committed catalog label, same as /v1/symbols; may be empty/Unknown, not point-in-time metadata."
                          }
                        },
                        "required": [
                          "missing_bars",
                          "input_first",
                          "calendar_covered_through_year",
                          "sector",
                          "board"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "409": {
            "description": "Expired/foreign scan: discard scan_id/cursor and restart page 0."
          },
          "429": {
            "description": "research_scan_busy or shared RPS exceeded. Busy responses include Retry-After seconds; wait before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Integer seconds before retry; present on research_scan_busy (currently 1).",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          }
        },
        "description": "Scan the entire catalog, then paginate matches; interval 1d/1h/2h/4h/1w/1mo, signal all or a named signal, limit 1-100 (default 25). Readiness uses the latest 200 observed selected-interval bars: at least 35 bars, valid candles, latest expected closed bar present, no expected gaps within that seed. With signal=all this matches an empty-condition POST over the same universe/cutoff when all referenced metrics fit the 200-bar seed. POST extends its seed for longer metrics; Research uses a separate 1000-bar seed. excluded_unready is a count only, not per-symbol status; excluded symbols are omitted. Page 0 returns scan_id; continue with next_cursor and scan_id. pagination_scope=matches; unknown/repeated query parameters return 422; missing/expired pagination returns 409. Independent snapshots may differ after corrections or cutoff changes. On 409, discard scan_id/cursor and restart page 0: the snapshot expired or belongs to another principal. On 429 research_scan_busy, wait Retry-After seconds (currently 1), then retry the same request with bounded backoff/jitter; do not issue parallel retries. Shared RPS 429 is a separate admission limit. The 180-second cold-scan deadline is an execution limit, not the retry delay. missing_bars is a sorted array of expected-but-absent bar labels (RFC3339 WIB), computed from the SAME calendar as missing_expected_bars; its length equals the count for evaluated histories. Daily entries are midnight dates, intraday entries are bucket labels, weekly/monthly entries are period starts. Scope starts at input_first in the observed seed and ends at closed_before; not a lifetime gap inventory. Empty/no_data or invalid_data histories return [] without proving completeness. Known holidays and non-session slots are excluded; calendar_covered_through_year is 2026, with the existing weekday approximation outside maintained holiday coverage. A missing expected bar is not proof of ingestion failure, pre-listing eligibility, suspension or tradability. Rows include sector and board from /v1/symbols; no sector filter is supported."
      },
      "post": {
        "tags": [
          "Screener"
        ],
        "summary": "Dynamic Screener",
        "description": "Filter the selected catalog universe, sort globally, paginate matches. columns is required (1-16); omission returns 422 invalid_screen_request before scan/cache setup or compute-slot acquisition. Readiness matches GET signal=all for an empty-condition screen whose referenced metrics fit 200 bars: at least 35 bars, valid candles, latest expected closed bar present, no expected gaps in the seed. The seed is max(200, required metric warm-up plus one previous bar), considering columns, conditions and sort_by. Examples: sma_200 needs 201 bars; sma_400 needs 401. These longer metrics deliberately may exclude more symbols than GET. Short-metric values now use the same 200-bar seed as GET, not the former 1000-bar Research seed; recursive indicator values may change. Custom indicator periods can also extend warm-up. Response lookback_bars reports the seed. symbols only selects the catalog subset, including on 1d; unknown symbols return 422. Unready symbols are omitted and counted in excluded_unready only: no per-symbol reason/status is returned, so a missing symbol cannot be distinguished from a condition non-match from that row list alone. Later pages repeat the definition plus cursor/scan_id; 409 means expired results, 429 busy/RPS. Cold deadline 180 seconds. Prices/thresholds are Decimal strings. On 409, discard scan_id/cursor and restart page 0: the snapshot expired or belongs to another principal. On 429 research_scan_busy, wait Retry-After seconds (currently 1), then retry the same request with bounded backoff/jitter; do not issue parallel retries. Shared RPS 429 is a separate admission limit. The 180-second cold-scan deadline is an execution limit, not the retry delay. missing_bars is a sorted array of expected-but-absent bar labels (RFC3339 WIB), computed from the SAME calendar as missing_expected_bars; its length equals the count for evaluated histories. Daily entries are midnight dates, intraday entries are bucket labels, weekly/monthly entries are period starts. Scope starts at input_first in the observed seed and ends at closed_before; not a lifetime gap inventory. Empty/no_data or invalid_data histories return [] without proving completeness. Known holidays and non-session slots are excluded; calendar_covered_through_year is 2026, with the existing weekday approximation outside maintained holiday coverage. A missing expected bar is not proof of ingestion failure, pre-listing eligibility, suspension or tradability. Rows include sector and board from /v1/symbols; no sector filter is supported.",
        "operationId": "dynamic_screener_v1_screener_post",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScreenRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScreenResponse"
                }
              }
            }
          },
          "409": {
            "description": "Expired/foreign scan: discard scan_id/cursor and restart page 0."
          },
          "413": {
            "description": "Request body exceeds 32 KiB"
          },
          "429": {
            "description": "research_scan_busy or shared RPS exceeded. Busy responses include Retry-After seconds; wait before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Integer seconds before retry; present on research_scan_busy (currently 1).",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "503": {
            "description": "Publication held, data unavailable, or three-minute scan deadline exceeded"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/coverage": {
      "get": {
        "tags": [
          "Coverage"
        ],
        "summary": "Coverage",
        "operationId": "coverage_v1_coverage_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": true,
                  "type": "object",
                  "title": "Response Coverage V1 Coverage Get"
                }
              }
            }
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "description": " rows_estimated=true: rows=max(0, Timescale approximate_row_count(table) minus an exact count at/after closed_before). Table statistics can be stale and over/under-count; no numerical error bound or completeness guarantee. Reads/cache may lag updates (60s). symbols counts only the latest stored session, not listing or suspension status."
      }
    },
    "/v1/research/{symbol}": {
      "post": {
        "tags": [
          "Research"
        ],
        "summary": "Research",
        "description": "Configurable Decimal values and observed chart bars; same seed as POST screening. missing_bars is a sorted array of expected-but-absent bar labels (RFC3339 WIB), computed from the SAME calendar as missing_expected_bars; its length equals the count for evaluated histories. Daily entries are midnight dates, intraday entries are bucket labels, weekly/monthly entries are period starts. Scope starts at input_first in the observed seed and ends at closed_before; not a lifetime gap inventory. Empty/no_data or invalid_data histories return [] without proving completeness. Known holidays and non-session slots are excluded; calendar_covered_through_year is 2026, with the existing weekday approximation outside maintained holiday coverage. A missing expected bar is not proof of ingestion failure, pre-listing eligibility, suspension or tradability.",
        "operationId": "research_v1_research__symbol__post",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "symbol",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Z]{4}$",
              "title": "Symbol"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScreenDefinition-Input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/broker-summary/{symbol}": {
      "get": {
        "tags": [
          "Market context"
        ],
        "summary": "Daily broker summary with buy/sell concentration",
        "operationId": "broker_summary_v1_get",
        "parameters": [
          {
            "name": "symbol",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Z]{4}$"
            }
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "date"
                },
                {
                  "type": "null"
                }
              ],
              "title": "From"
            }
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "date"
                },
                {
                  "type": "null"
                }
              ],
              "title": "To"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 90
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrokerSummaryResponse"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Shared RPS exceeded or market reads busy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Publication held or data temporarily unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          }
        },
        "description": "Concentration uses reported net values separately by side: top-1/3/5 percentages and HHI (0\u201310000). Duplicate broker codes are combined. Empty/invalid inputs yield null metrics with status; no ownership or smart-money inference. Existing top1_pct/top3_pct retain their legacy detector meaning, distinct from concentration. Prior WIB dates only, newest first. start/end default to the last 90 calendar days; maximum 366-day window; limit 1\u2013365, default 90. Served natively by idx-research: SELECT-only stored reads and Go computation. One shared compute slot; busy returns 429 with Retry-After: 1. 20-second deadline; 8 MiB response cap; stored contexts cached 60s per principal. API-key authentication/shared user RPS/publication gate apply. Unknown or repeated query parameters return 422. Intraday flow explicitly out of scope: interval=1h is rejected with 422. Daily values are previous-session context, NOT current-session flow. Read each row's date; weekends, holidays and ingestion lag can make it older than the immediately preceding session. No hourly broker concentration is offered. Response array key is rows, not data.",
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/v1/foreign-flow/{symbol}": {
      "get": {
        "tags": [
          "Market context"
        ],
        "summary": "Daily foreign flow",
        "operationId": "foreign_flow_v1_get",
        "parameters": [
          {
            "name": "symbol",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Z]{4}$"
            }
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "date"
                },
                {
                  "type": "null"
                }
              ],
              "title": "From"
            }
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "date"
                },
                {
                  "type": "null"
                }
              ],
              "title": "To"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 90
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForeignFlowResponse"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Shared RPS exceeded or market reads busy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Publication held or data temporarily unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          }
        },
        "description": "Prior WIB dates only, newest first. Decimal IDR values. start/end default to the last 90 calendar days; maximum 366-day window; limit 1\u2013365, default 90. Served natively by idx-research: SELECT-only stored reads and Go computation. One shared compute slot; busy returns 429 with Retry-After: 1. 20-second deadline; 8 MiB response cap; stored contexts cached 60s per principal. API-key authentication/shared user RPS/publication gate apply. Unknown or repeated query parameters return 422. Intraday flow explicitly out of scope: interval=1h is rejected with 422. Daily values are previous-session context, NOT current-session flow. Read each row's date; weekends, holidays and ingestion lag can make it older than the immediately preceding session. No hourly broker concentration is offered. Response array key is rows, not data.",
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/v1/wyckoff/{symbol}": {
      "get": {
        "tags": [
          "Market context"
        ],
        "summary": "Wyckoff events with raw broker context",
        "operationId": "wyckoff_v1_get",
        "parameters": [
          {
            "name": "symbol",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Z]{4}$"
            }
          },
          {
            "name": "lookback_days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 365,
              "minimum": 1,
              "default": 180,
              "title": "Lookback Days"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WyckoffResponse"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Shared RPS exceeded or market reads busy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Publication held or data temporarily unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          }
        },
        "description": "OHLCV detects events/current phase; each event includes same-day raw broker concentration. Latest-bar broker_context is date-aligned; missing data stays unavailable. Phase is an OHLCV heuristic, not a broker-identity signal. lookback_days 1\u2013365, default 180. Prices, event strength and concentration are Decimal strings. Served natively by idx-research: SELECT-only stored reads and Go computation. One shared compute slot; busy returns 429 with Retry-After: 1. 20-second deadline; 8 MiB response cap; stored contexts cached 60s per principal. API-key authentication/shared user RPS/publication gate apply. Unknown or repeated query parameters return 422.",
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/v1/datasets/{symbol}": {
      "get": {
        "tags": [
          "Market context"
        ],
        "summary": "Per-symbol dataset manifest",
        "description": "Maintained dataset endpoints and observed freshness. All returned endpoint pointers use public /v1 paths. Counts/ranges are not completeness or suspension evidence. Operational/internal endpoints are never published. Served natively by idx-research: SELECT-only stored reads and Go computation. One shared compute slot; busy returns 429 with Retry-After: 1. 20-second deadline; 8 MiB response cap; stored contexts cached 60s per principal. API-key authentication/shared user RPS/publication gate apply. Unknown or repeated query parameters return 422. Dataset pointer. Stored eod/hourly entries have freshness={earliest_day,last_day,row_count}; empty stored data uses null dates and row_count=0. On-demand wyckoff_events freshness is null even with upstream data; null means no persisted aggregate, NOT zero rows or an error. Inspect kind and guard null before dereferencing. Dataset entries live in the datasets object, not an array.",
        "operationId": "datasets_v1_get",
        "parameters": [
          {
            "name": "symbol",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Z]{4}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SymbolIndexDTO"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Shared RPS exceeded or market reads busy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Publication held or data temporarily unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "detail"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "Candle": {
        "properties": {
          "symbol": {
            "type": "string",
            "title": "Symbol"
          },
          "time": {
            "type": "string",
            "format": "date-time",
            "title": "Time"
          },
          "open": {
            "type": "string",
            "title": "Open"
          },
          "high": {
            "type": "string",
            "title": "High"
          },
          "low": {
            "type": "string",
            "title": "Low"
          },
          "close": {
            "type": "string",
            "title": "Close"
          },
          "volume": {
            "type": "integer",
            "title": "Volume"
          },
          "value": {
            "type": "string",
            "title": "Value"
          },
          "source_bars": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Source Bars"
          },
          "short_session": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Short Session"
          },
          "end_time": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "End Time",
            "description": "Bucket label end, not settlement time; /v1/candles rows expose settled_at."
          },
          "indicators": {
            "anyOf": [
              {
                "additionalProperties": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Indicators"
          }
        },
        "type": "object",
        "required": [
          "symbol",
          "time",
          "open",
          "high",
          "low",
          "close",
          "volume",
          "value"
        ],
        "title": "Candle"
      },
      "CandlePage": {
        "properties": {
          "data": {
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Candle"
                },
                {
                  "type": "object",
                  "required": [
                    "settled_at",
                    "is_final"
                  ],
                  "properties": {
                    "settled_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Scheduled read eligibility in WIB; not upstream immutability or observed ingestion time."
                    },
                    "is_final": {
                      "type": "boolean",
                      "const": true,
                      "description": "Eligible at response as_of; true for returned closed-only candles."
                    }
                  }
                }
              ]
            },
            "type": "array",
            "title": "Data"
          },
          "symbol": {
            "type": "string",
            "title": "Symbol"
          },
          "interval": {
            "type": "string",
            "enum": [
              "1d",
              "1h",
              "2h",
              "4h",
              "1w",
              "1mo"
            ],
            "title": "Interval"
          },
          "timezone": {
            "type": "string",
            "title": "Timezone"
          },
          "volume_unit": {
            "type": "string",
            "title": "Volume Unit"
          },
          "price_currency": {
            "type": "string",
            "title": "Price Currency"
          },
          "finality": {
            "type": "string",
            "title": "Finality",
            "description": "closed_bars_only: every returned candle is eligible at as_of; late corrections remain possible."
          },
          "closed_before": {
            "type": "string",
            "format": "date-time",
            "title": "Closed Before",
            "description": "Exclusive source/label selection boundary, not per-row settlement time; use data[].settled_at."
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "title": "As Of"
          },
          "derived_from": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Derived From"
          },
          "bucket_policy": {
            "type": "string",
            "title": "Bucket Policy"
          },
          "missing_buckets": {
            "type": "integer",
            "title": "Missing Buckets"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          },
          "note": {
            "type": "string",
            "title": "Note"
          },
          "indicator_calculation": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Indicator Calculation"
          }
        },
        "type": "object",
        "required": [
          "data",
          "symbol",
          "interval",
          "timezone",
          "volume_unit",
          "price_currency",
          "finality",
          "closed_before",
          "as_of",
          "derived_from",
          "bucket_policy",
          "missing_buckets",
          "next_cursor",
          "note"
        ],
        "title": "CandlePage"
      },
      "HTTPValidationError": {
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ValidationError"
            },
            "type": "array",
            "title": "Detail"
          }
        },
        "type": "object",
        "title": "HTTPValidationError"
      },
      "IndicatorParameters": {
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "open",
              "high",
              "low",
              "close",
              "hl2",
              "hlc3",
              "ohlc4"
            ],
            "title": "Source",
            "default": "close"
          },
          "rsi": {
            "type": "integer",
            "maximum": 200.0,
            "minimum": 1.0,
            "title": "Rsi",
            "default": 14
          },
          "atr": {
            "type": "integer",
            "maximum": 200.0,
            "minimum": 1.0,
            "title": "Atr",
            "default": 14
          },
          "macd_fast": {
            "type": "integer",
            "maximum": 200.0,
            "minimum": 1.0,
            "title": "Macd Fast",
            "default": 12
          },
          "macd_slow": {
            "type": "integer",
            "maximum": 200.0,
            "minimum": 1.0,
            "title": "Macd Slow",
            "default": 26
          },
          "macd_signal": {
            "type": "integer",
            "maximum": 200.0,
            "minimum": 1.0,
            "title": "Macd Signal",
            "default": 9
          },
          "stoch_period": {
            "type": "integer",
            "maximum": 200.0,
            "minimum": 1.0,
            "title": "Stoch Period",
            "default": 14
          },
          "stoch_k": {
            "type": "integer",
            "maximum": 200.0,
            "minimum": 1.0,
            "title": "Stoch K",
            "default": 3
          },
          "stoch_d": {
            "type": "integer",
            "maximum": 200.0,
            "minimum": 1.0,
            "title": "Stoch D",
            "default": 3
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "IndicatorParameters"
      },
      "ResearchResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ResearchRow"
          },
          "chart": {
            "items": {
              "$ref": "#/components/schemas/Candle"
            },
            "type": "array",
            "title": "Chart"
          },
          "definition": {
            "$ref": "#/components/schemas/ScreenDefinition-Output"
          },
          "computed_at": {
            "type": "string",
            "format": "date-time",
            "title": "Computed At"
          },
          "closed_before": {
            "type": "string",
            "format": "date-time",
            "title": "Closed Before"
          },
          "timezone": {
            "type": "string",
            "title": "Timezone"
          },
          "volume_unit": {
            "type": "string",
            "title": "Volume Unit"
          },
          "finality": {
            "type": "string",
            "title": "Finality"
          },
          "lookback_bars": {
            "type": "integer",
            "title": "Lookback Bars"
          },
          "note": {
            "type": "string",
            "title": "Note"
          }
        },
        "type": "object",
        "required": [
          "data",
          "chart",
          "definition",
          "computed_at",
          "closed_before",
          "timezone",
          "volume_unit",
          "finality",
          "lookback_bars",
          "note"
        ],
        "title": "ResearchResponse"
      },
      "ResearchRow": {
        "properties": {
          "symbol": {
            "type": "string",
            "title": "Symbol"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "status": {
            "type": "string",
            "enum": [
              "no_data",
              "invalid_data",
              "stale",
              "coverage_gaps",
              "insufficient_history",
              "ready"
            ],
            "title": "Status"
          },
          "bar_time": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Bar Time"
          },
          "input_bars": {
            "type": "integer",
            "title": "Input Bars"
          },
          "minimum_bars": {
            "type": "integer",
            "title": "Minimum Bars"
          },
          "lookback_bars": {
            "type": "integer",
            "title": "Lookback Bars"
          },
          "missing_expected_bars": {
            "type": "integer",
            "title": "Missing Expected Bars"
          },
          "values": {
            "additionalProperties": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            },
            "type": "object",
            "title": "Values"
          },
          "missing_bars": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "date-time"
            },
            "description": "missing_bars is a sorted array of expected-but-absent bar labels (RFC3339 WIB), computed from the SAME calendar as missing_expected_bars; its length equals the count for evaluated histories. Daily entries are midnight dates, intraday entries are bucket labels, weekly/monthly entries are period starts. Scope starts at input_first in the observed seed and ends at closed_before; not a lifetime gap inventory. Empty/no_data or invalid_data histories return [] without proving completeness. Known holidays and non-session slots are excluded; calendar_covered_through_year is 2026, with the existing weekday approximation outside maintained holiday coverage. A missing expected bar is not proof of ingestion failure, pre-listing eligibility, suspension or tradability."
          },
          "input_first": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "description": "First observed bar in the evaluated seed; null without history."
          },
          "calendar_covered_through_year": {
            "type": "integer",
            "description": "Current maintained holiday coverage year; not complete historical listing/status coverage."
          },
          "sector": {
            "type": "string",
            "description": "Committed catalog label, same as /v1/symbols; may be empty/Unknown, not point-in-time metadata."
          },
          "board": {
            "type": "string",
            "description": "Committed catalog label, same as /v1/symbols; may be empty/Unknown, not point-in-time metadata."
          }
        },
        "type": "object",
        "required": [
          "symbol",
          "status",
          "bar_time",
          "input_bars",
          "minimum_bars",
          "lookback_bars",
          "missing_expected_bars",
          "values",
          "missing_bars",
          "input_first",
          "calendar_covered_through_year",
          "sector",
          "board"
        ],
        "title": "ResearchRow"
      },
      "ScreenCondition-Input": {
        "properties": {
          "field": {
            "type": "string",
            "maxLength": 32,
            "minLength": 1,
            "title": "Field",
            "description": "Accepted metrics: open, high, low, close, volume, value, change_pct, rsi, atr, rsi_14, atr_14, macd, macd_signal, macd_histogram, stoch_k, stoch_d, sma_N, ema_N, vol_sma_N (integer N=1..800, no leading zeros). rsi_14/atr_14 use fixed period 14, matching /v1/candles indicator names; rsi/atr use parameters.rsi/atr (default 14). Unsupported names return 422."
          },
          "op": {
            "type": "string",
            "enum": [
              "gt",
              "gte",
              "lt",
              "lte",
              "eq",
              "between",
              "crosses_above",
              "crosses_below"
            ],
            "title": "Op"
          },
          "other": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 32,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Other",
            "description": "Accepted metrics: open, high, low, close, volume, value, change_pct, rsi, atr, rsi_14, atr_14, macd, macd_signal, macd_histogram, stoch_k, stoch_d, sma_N, ema_N, vol_sma_N (integer N=1..800, no leading zeros). rsi_14/atr_14 use fixed period 14, matching /v1/candles indicator names; rsi/atr use parameters.rsi/atr (default 14). Unsupported names return 422."
          },
          "value": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Value",
            "description": "Finite decimal string or integer; maximum 20 integer digits and 8 decimal places. JSON floats rejected."
          },
          "upper": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Upper",
            "description": "Inclusive upper bound for between; same numeric limits as value."
          },
          "multiplier": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              }
            ],
            "title": "Multiplier",
            "description": "Finite decimal string or integer, 0-1000, at most 8 decimal places; JSON floats rejected.",
            "default": "1"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "field",
          "op"
        ],
        "title": "ScreenCondition"
      },
      "ScreenCondition-Output": {
        "properties": {
          "field": {
            "type": "string",
            "maxLength": 32,
            "minLength": 1,
            "title": "Field",
            "description": "Accepted metrics: open, high, low, close, volume, value, change_pct, rsi, atr, rsi_14, atr_14, macd, macd_signal, macd_histogram, stoch_k, stoch_d, sma_N, ema_N, vol_sma_N (integer N=1..800, no leading zeros). rsi_14/atr_14 use fixed period 14, matching /v1/candles indicator names; rsi/atr use parameters.rsi/atr (default 14). Unsupported names return 422."
          },
          "op": {
            "type": "string",
            "enum": [
              "gt",
              "gte",
              "lt",
              "lte",
              "eq",
              "between",
              "crosses_above",
              "crosses_below"
            ],
            "title": "Op"
          },
          "other": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 32,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Other",
            "description": "Accepted metrics: open, high, low, close, volume, value, change_pct, rsi, atr, rsi_14, atr_14, macd, macd_signal, macd_histogram, stoch_k, stoch_d, sma_N, ema_N, vol_sma_N (integer N=1..800, no leading zeros). rsi_14/atr_14 use fixed period 14, matching /v1/candles indicator names; rsi/atr use parameters.rsi/atr (default 14). Unsupported names return 422."
          },
          "value": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Value",
            "description": "Finite decimal string or integer; maximum 20 integer digits and 8 decimal places. JSON floats rejected."
          },
          "upper": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Upper",
            "description": "Inclusive upper bound for between; same numeric limits as value."
          },
          "multiplier": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              }
            ],
            "type": "string",
            "pattern": "^(?!^[-+.]*$)[+-]?0*(?:\\d{0,4}|(?=[\\d.]{1,13}0*$)\\d{0,4}\\.\\d{0,8}0*$)",
            "title": "Multiplier",
            "description": "Finite decimal string or integer, 0-1000, at most 8 decimal places; JSON floats rejected.",
            "default": "1"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "field",
          "op"
        ],
        "title": "ScreenCondition"
      },
      "ScreenDefinition-Input": {
        "properties": {
          "interval": {
            "type": "string",
            "enum": [
              "1d",
              "1h",
              "2h",
              "4h",
              "1w",
              "1mo"
            ],
            "title": "Interval",
            "default": "1d"
          },
          "parameters": {
            "$ref": "#/components/schemas/IndicatorParameters"
          },
          "columns": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "maxItems": 16,
            "minItems": 1,
            "title": "Columns"
          },
          "conditions": {
            "items": {
              "$ref": "#/components/schemas/ScreenCondition-Input"
            },
            "type": "array",
            "maxItems": 12,
            "title": "Conditions"
          },
          "mode": {
            "type": "string",
            "enum": [
              "and",
              "or"
            ],
            "title": "Mode",
            "default": "and"
          },
          "sort_by": {
            "type": "string",
            "maxLength": 32,
            "title": "Sort By",
            "default": "symbol"
          },
          "sort_direction": {
            "type": "string",
            "enum": [
              "asc",
              "desc"
            ],
            "title": "Sort Direction",
            "default": "asc"
          },
          "symbols": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "maxItems": 1000,
            "title": "Symbols"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "ScreenDefinition"
      },
      "ScreenDefinition-Output": {
        "properties": {
          "interval": {
            "type": "string",
            "enum": [
              "1d",
              "1h",
              "2h",
              "4h",
              "1w",
              "1mo"
            ],
            "title": "Interval",
            "default": "1d"
          },
          "parameters": {
            "$ref": "#/components/schemas/IndicatorParameters"
          },
          "columns": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "maxItems": 16,
            "minItems": 1,
            "title": "Columns"
          },
          "conditions": {
            "items": {
              "$ref": "#/components/schemas/ScreenCondition-Output"
            },
            "type": "array",
            "maxItems": 12,
            "title": "Conditions"
          },
          "mode": {
            "type": "string",
            "enum": [
              "and",
              "or"
            ],
            "title": "Mode",
            "default": "and"
          },
          "sort_by": {
            "type": "string",
            "maxLength": 32,
            "title": "Sort By",
            "default": "symbol"
          },
          "sort_direction": {
            "type": "string",
            "enum": [
              "asc",
              "desc"
            ],
            "title": "Sort Direction",
            "default": "asc"
          },
          "symbols": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "maxItems": 1000,
            "title": "Symbols"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "ScreenDefinition"
      },
      "ScreenRequest": {
        "properties": {
          "interval": {
            "type": "string",
            "enum": [
              "1d",
              "1h",
              "2h",
              "4h",
              "1w",
              "1mo"
            ],
            "title": "Interval",
            "default": "1d"
          },
          "parameters": {
            "$ref": "#/components/schemas/IndicatorParameters"
          },
          "columns": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "maxItems": 16,
            "minItems": 1,
            "title": "Columns",
            "description": "Accepted metrics: open, high, low, close, volume, value, change_pct, rsi, atr, rsi_14, atr_14, macd, macd_signal, macd_histogram, stoch_k, stoch_d, sma_N, ema_N, vol_sma_N (integer N=1..800, no leading zeros). rsi_14/atr_14 use fixed period 14, matching /v1/candles indicator names; rsi/atr use parameters.rsi/atr (default 14). Unsupported names return 422."
          },
          "conditions": {
            "items": {
              "$ref": "#/components/schemas/ScreenCondition-Input"
            },
            "type": "array",
            "maxItems": 12,
            "title": "Conditions"
          },
          "mode": {
            "type": "string",
            "enum": [
              "and",
              "or"
            ],
            "title": "Mode",
            "default": "and"
          },
          "sort_by": {
            "type": "string",
            "maxLength": 32,
            "title": "Sort By",
            "default": "symbol",
            "description": "Accepted metrics: open, high, low, close, volume, value, change_pct, rsi, atr, rsi_14, atr_14, macd, macd_signal, macd_histogram, stoch_k, stoch_d, sma_N, ema_N, vol_sma_N (integer N=1..800, no leading zeros). rsi_14/atr_14 use fixed period 14, matching /v1/candles indicator names; rsi/atr use parameters.rsi/atr (default 14). Unsupported names return 422. sort_by additionally accepts symbol."
          },
          "sort_direction": {
            "type": "string",
            "enum": [
              "asc",
              "desc"
            ],
            "title": "Sort Direction",
            "default": "asc"
          },
          "symbols": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "maxItems": 1000,
            "title": "Symbols"
          },
          "limit": {
            "type": "integer",
            "maximum": 100.0,
            "minimum": 1.0,
            "title": "Limit",
            "default": 25
          },
          "cursor": {
            "type": "integer",
            "maximum": 1000.0,
            "minimum": 0.0,
            "title": "Cursor",
            "default": 0
          },
          "scan_id": {
            "anyOf": [
              {
                "type": "string",
                "pattern": "^[0-9a-f]{32}$"
              },
              {
                "type": "null"
              }
            ],
            "title": "Scan Id"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "ScreenRequest",
        "required": [
          "columns"
        ]
      },
      "ScreenResponse": {
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/ResearchRow"
            },
            "type": "array",
            "title": "Data"
          },
          "definition": {
            "$ref": "#/components/schemas/ScreenDefinition-Output"
          },
          "matched": {
            "type": "integer",
            "title": "Matched"
          },
          "scanned": {
            "type": "integer",
            "title": "Scanned"
          },
          "excluded_unready": {
            "type": "integer",
            "title": "Excluded Unready"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          },
          "scan_id": {
            "type": "string",
            "title": "Scan Id"
          },
          "computed_at": {
            "type": "string",
            "format": "date-time",
            "title": "Computed At"
          },
          "closed_before": {
            "type": "string",
            "format": "date-time",
            "title": "Closed Before"
          },
          "timezone": {
            "type": "string",
            "title": "Timezone"
          },
          "volume_unit": {
            "type": "string",
            "title": "Volume Unit"
          },
          "finality": {
            "type": "string",
            "title": "Finality"
          },
          "lookback_bars": {
            "type": "integer",
            "title": "Lookback Bars"
          },
          "note": {
            "type": "string",
            "title": "Note"
          }
        },
        "type": "object",
        "required": [
          "data",
          "definition",
          "matched",
          "scanned",
          "excluded_unready",
          "next_cursor",
          "scan_id",
          "computed_at",
          "closed_before",
          "timezone",
          "volume_unit",
          "finality",
          "lookback_bars",
          "note"
        ],
        "title": "ScreenResponse"
      },
      "ValidationError": {
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                }
              ]
            },
            "type": "array",
            "title": "Location"
          },
          "msg": {
            "type": "string",
            "title": "Message"
          },
          "type": {
            "type": "string",
            "title": "Error Type"
          },
          "input": {
            "title": "Input"
          },
          "ctx": {
            "type": "object",
            "title": "Context"
          }
        },
        "type": "object",
        "required": [
          "loc",
          "msg",
          "type"
        ],
        "title": "ValidationError"
      },
      "BrokerSummaryResponse": {
        "properties": {
          "symbol": {
            "type": "string",
            "title": "Symbol"
          },
          "rows": {
            "items": {
              "$ref": "#/components/schemas/BrokerSummaryRow"
            },
            "type": "array",
            "title": "Rows"
          }
        },
        "type": "object",
        "required": [
          "symbol",
          "rows"
        ],
        "title": "BrokerSummaryResponse"
      },
      "BrokerSummaryRow": {
        "properties": {
          "day": {
            "type": "string",
            "format": "date",
            "title": "Day"
          },
          "top1_pct": {
            "type": "string",
            "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
            "title": "Top1 Pct"
          },
          "top3_pct": {
            "type": "string",
            "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
            "title": "Top3 Pct"
          },
          "top1_accdist": {
            "type": "string",
            "title": "Top1 Accdist"
          },
          "payload": {
            "additionalProperties": true,
            "type": "object",
            "title": "Payload"
          },
          "concentration": {
            "$ref": "#/components/schemas/BrokerConcentration"
          }
        },
        "type": "object",
        "required": [
          "day",
          "top1_pct",
          "top3_pct",
          "top1_accdist",
          "payload",
          "concentration"
        ],
        "title": "BrokerSummaryRow"
      },
      "BrokerConcentration": {
        "properties": {
          "basis": {
            "type": "string",
            "const": "reported_net_value",
            "title": "Basis"
          },
          "coverage": {
            "type": "string",
            "const": "reported_brokers_only",
            "title": "Coverage"
          },
          "buy": {
            "$ref": "#/components/schemas/ConcentrationSide"
          },
          "sell": {
            "$ref": "#/components/schemas/ConcentrationSide"
          }
        },
        "type": "object",
        "required": [
          "basis",
          "coverage",
          "buy",
          "sell"
        ],
        "title": "BrokerConcentration"
      },
      "ConcentrationSide": {
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ready",
              "no_data",
              "invalid_payload"
            ],
            "title": "Status"
          },
          "broker_count": {
            "type": "integer",
            "title": "Broker Count"
          },
          "total_net_value": {
            "anyOf": [
              {
                "type": "string",
                "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"
              },
              {
                "type": "null"
              }
            ],
            "title": "Total Net Value"
          },
          "top1_pct": {
            "anyOf": [
              {
                "type": "string",
                "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"
              },
              {
                "type": "null"
              }
            ],
            "title": "Top1 Pct"
          },
          "top3_pct": {
            "anyOf": [
              {
                "type": "string",
                "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"
              },
              {
                "type": "null"
              }
            ],
            "title": "Top3 Pct"
          },
          "top5_pct": {
            "anyOf": [
              {
                "type": "string",
                "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"
              },
              {
                "type": "null"
              }
            ],
            "title": "Top5 Pct"
          },
          "hhi": {
            "anyOf": [
              {
                "type": "string",
                "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"
              },
              {
                "type": "null"
              }
            ],
            "title": "Hhi"
          }
        },
        "type": "object",
        "required": [
          "status",
          "broker_count",
          "total_net_value",
          "top1_pct",
          "top3_pct",
          "top5_pct",
          "hhi"
        ],
        "title": "ConcentrationSide"
      },
      "ForeignFlowResponse": {
        "properties": {
          "symbol": {
            "type": "string",
            "title": "Symbol"
          },
          "rows": {
            "items": {
              "$ref": "#/components/schemas/ForeignFlowRow"
            },
            "type": "array",
            "title": "Rows"
          }
        },
        "type": "object",
        "required": [
          "symbol",
          "rows"
        ],
        "title": "ForeignFlowResponse"
      },
      "ForeignFlowRow": {
        "properties": {
          "day": {
            "type": "string",
            "format": "date",
            "title": "Day"
          },
          "foreign_buy": {
            "type": "string",
            "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
            "title": "Foreign Buy"
          },
          "foreign_sell": {
            "type": "string",
            "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
            "title": "Foreign Sell"
          },
          "net_foreign": {
            "type": "string",
            "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
            "title": "Net Foreign"
          }
        },
        "type": "object",
        "required": [
          "day",
          "foreign_buy",
          "foreign_sell",
          "net_foreign"
        ],
        "title": "ForeignFlowRow"
      },
      "WyckoffResponse": {
        "properties": {
          "symbol": {
            "type": "string",
            "title": "Symbol"
          },
          "basis": {
            "type": "string",
            "title": "Basis"
          },
          "broker_context": {
            "additionalProperties": true,
            "type": "object",
            "title": "Broker Context"
          },
          "rows": {
            "items": {
              "$ref": "#/components/schemas/WyckoffEventRow"
            },
            "type": "array",
            "title": "Rows"
          },
          "phase_summary": {
            "additionalProperties": true,
            "type": "object",
            "title": "Phase Summary"
          }
        },
        "type": "object",
        "required": [
          "symbol",
          "basis",
          "broker_context",
          "rows",
          "phase_summary"
        ],
        "title": "WyckoffResponse"
      },
      "WyckoffEventRow": {
        "properties": {
          "day": {
            "type": "string",
            "format": "date",
            "title": "Day"
          },
          "event_type": {
            "type": "string",
            "title": "Event Type"
          },
          "strength": {
            "type": "string",
            "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$",
            "title": "Strength"
          },
          "details": {
            "additionalProperties": true,
            "type": "object",
            "title": "Details"
          }
        },
        "type": "object",
        "required": [
          "day",
          "event_type",
          "strength",
          "details"
        ],
        "title": "WyckoffEventRow"
      },
      "SymbolIndexDTO": {
        "properties": {
          "symbol": {
            "type": "string",
            "title": "Symbol"
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "title": "As Of"
          },
          "datasets": {
            "additionalProperties": {
              "$ref": "#/components/schemas/DatasetEntry"
            },
            "type": "object",
            "title": "Datasets"
          }
        },
        "type": "object",
        "required": [
          "symbol",
          "as_of",
          "datasets"
        ],
        "title": "SymbolIndexDTO"
      },
      "DatasetEntry": {
        "properties": {
          "endpoint": {
            "type": "string",
            "title": "Endpoint"
          },
          "kind": {
            "type": "string",
            "title": "Kind"
          },
          "freshness": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DatasetFreshness"
              },
              {
                "type": "null"
              }
            ],
            "description": "Dataset pointer. Stored eod/hourly entries have freshness={earliest_day,last_day,row_count}; empty stored data uses null dates and row_count=0. On-demand wyckoff_events freshness is null even with upstream data; null means no persisted aggregate, NOT zero rows or an error. Inspect kind and guard null before dereferencing."
          }
        },
        "type": "object",
        "required": [
          "endpoint",
          "kind"
        ],
        "title": "DatasetEntry",
        "description": "Dataset pointer. Stored eod/hourly entries have freshness={earliest_day,last_day,row_count}; empty stored data uses null dates and row_count=0. On-demand wyckoff_events freshness is null even with upstream data; null means no persisted aggregate, NOT zero rows or an error. Inspect kind and guard null before dereferencing."
      },
      "DatasetFreshness": {
        "properties": {
          "last_day": {
            "anyOf": [
              {
                "type": "string",
                "format": "date"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Day"
          },
          "earliest_day": {
            "anyOf": [
              {
                "type": "string",
                "format": "date"
              },
              {
                "type": "null"
              }
            ],
            "title": "Earliest Day"
          },
          "row_count": {
            "type": "integer",
            "title": "Row Count",
            "default": 0
          }
        },
        "type": "object",
        "title": "DatasetFreshness",
        "description": "Per-dataset freshness summary.\n\n``last_day`` / ``earliest_day`` are WIB trading days (``YYYY-MM-DD``).\n``row_count`` is the symbol's row count in the source table.\nAll fields are nullable \u2014 a symbol with no rows yet returns nulls."
      },
      "CatalogSymbol": {
        "type": "object",
        "required": [
          "symbol",
          "name",
          "sector",
          "board",
          "lot_size",
          "listing_date"
        ],
        "properties": {
          "symbol": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "sector": {
            "type": "string"
          },
          "board": {
            "type": "string"
          },
          "lot_size": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1
              },
              {
                "type": "null"
              }
            ],
            "description": "Shares per lot from committed catalog; null when absent."
          },
          "listing_date": {
            "anyOf": [
              {
                "type": "string",
                "format": "date"
              },
              {
                "type": "null"
              }
            ],
            "description": "Recorded listing date, not independently verified; missing/1900-01-01 becomes null."
          }
        }
      }
    },
    "securitySchemes": {
      "HTTPBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "InvestDX API key"
      }
    }
  },
  "servers": [
    {
      "url": "https://api.musaddiqr.com"
    }
  ]
}
