{
  "openapi": "3.1.0",
  "info": {
    "title": "QM Screener API",
    "version": "1.0.0",
    "summary": "Daily momentum screening results for US equities.",
    "description": "A daily momentum stock screener. Six published trend and momentum methodologies \u2014 Qullamaggie, Minervini trend template, VCP, gappers, and two Mike Webster proxies \u2014 run against the full US equity universe after every trading-day close.\n\nThree endpoints are public; the rest need a subscriber token \u2014 see https://qmscreener.com/auth.md. Results are end-of-day and are not financial advice.\n\n## Versioning\n\nThe current major version is `v1`. Pin it: every path below is canonical at `/api/v1/...`, and the unversioned `/api/...` form is a permanent alias for the same handler (the dashboard's own JavaScript has always used it). Every API response carries `API-Version` so a caller can tell what answered it.\n\nA breaking change gets a new prefix \u2014 `/api/v2/...` \u2014 rather than changing this one. When `v1` is eventually retired, its responses will carry RFC 8594 `Deprecation` and `Sunset` headers for at least 180 days before the endpoint stops answering, and the sunset date will be stated here. Additive changes \u2014 a new field, a new endpoint, a new optional parameter \u2014 are not breaking and ship without a version bump.\n\n## Rate limits\n\nResponses carry the RFC 9331 `RateLimit` and `RateLimit-Policy` headers; a 429 adds `Retry-After` in seconds. Self-throttle on those rather than on a fixed sleep.",
    "contact": {
      "name": "QM Screener support",
      "email": "support@qmscreener.com",
      "url": "https://qmscreener.com/contact"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://qmscreener.com/terms"
    },
    "termsOfService": "https://qmscreener.com/terms"
  },
  "servers": [
    {
      "url": "https://qmscreener.com",
      "description": "Production"
    }
  ],
  "x-api-version": "1",
  "x-version-policy": {
    "current": "v1",
    "path_prefix": "/api/v1",
    "unversioned_alias": "/api",
    "response_header": "API-Version",
    "breaking_change_policy": "new path prefix",
    "deprecation_headers": [
      "Deprecation",
      "Sunset"
    ],
    "minimum_notice_days": 180
  },
  "externalDocs": {
    "description": "Agent documentation",
    "url": "https://qmscreener.com/llms.txt"
  },
  "tags": [
    {
      "name": "public",
      "description": "No authentication required."
    },
    {
      "name": "screens",
      "description": "Screening results."
    },
    {
      "name": "market",
      "description": "Price and sector data."
    },
    {
      "name": "watchlist",
      "description": "The caller's saved tickers."
    }
  ],
  "paths": {
    "/api/v1/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Liveness probe",
        "description": "Returns 200 whenever the service is up. No authentication, no rate limit.",
        "tags": [
          "public"
        ],
        "security": [],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "is_admin": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/preview": {
      "get": {
        "operationId": "getTodaysTopSetups",
        "summary": "Today's leading setups, per screen",
        "description": "The free teaser: for each screen, the few highest-ranked passing tickers from the most recent run, plus how many passed in total. Use this to answer 'what is setting up today' without a subscription. The full lists need a token.",
        "tags": [
          "public",
          "screens"
        ],
        "security": [],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Per-screen teaser.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Preview"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sector-strength": {
      "get": {
        "operationId": "getSectorStrength",
        "summary": "Sector ETF performance against SPY",
        "description": "Trailing returns for 21 US sector and industry ETFs measured against SPY, from the most recent run. Public because it is a fact about the market rather than a screening result.",
        "tags": [
          "public",
          "market"
        ],
        "security": [],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Sector tape. An empty object means no screen has run yet on this deployment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/screens": {
      "get": {
        "operationId": "listScreens",
        "summary": "Registered screens and their metadata",
        "description": "Every screen's identifier, methodology description, result filename, default sort, and the filter controls the dashboard renders for it.",
        "tags": [
          "screens"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Screen registry.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "screens": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Screen"
                      }
                    },
                    "default": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "No active subscription.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/{screen}_results.json": {
      "get": {
        "operationId": "getScreenResults",
        "summary": "One screen's full result set",
        "description": "Every passing ticker for the named screen from the latest run, plus the near-miss list (passed everything except relative strength) and the mean-reversion list. Each row carries the metric values that decided it.",
        "tags": [
          "screens"
        ],
        "parameters": [
          {
            "name": "screen",
            "in": "path",
            "required": true,
            "description": "Screen identifier.",
            "schema": {
              "type": "string",
              "enum": [
                "qullamaggie",
                "minervini_trend",
                "vcp",
                "gappers",
                "webster_recession",
                "webster_swing"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Result document.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScreenResults"
                }
              }
            }
          },
          "401": {
            "description": "No or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "No active subscription.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such screen.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/history": {
      "get": {
        "operationId": "getPriceHistory",
        "summary": "Daily OHLCV bars for one ticker",
        "description": "Ascending daily bars for a single US symbol, reconstructed from cached end-of-day summaries. An unknown symbol returns an empty `bars` array with 200; only a malformed symbol is a 400.",
        "tags": [
          "market"
        ],
        "parameters": [
          {
            "name": "ticker",
            "in": "query",
            "required": true,
            "description": "US equity symbol, e.g. `NVDA`.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z.\\-]{1,10}$"
            },
            "example": "NVDA"
          }
        ],
        "responses": {
          "200": {
            "description": "Daily bars.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/History"
                }
              }
            }
          },
          "400": {
            "description": "Malformed ticker.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "No active subscription.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/watchlist": {
      "get": {
        "operationId": "getWatchlist",
        "summary": "The caller's saved tickers",
        "description": "Every ticker the authenticated account has saved, as an array of symbols.",
        "tags": [
          "watchlist"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Saved tickers.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tickers": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "No active subscription.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "updateWatchlist",
        "summary": "Add or remove one ticker",
        "description": "Adds or removes a single symbol and echoes the resulting list.",
        "tags": [
          "watchlist"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ticker",
                  "action"
                ],
                "properties": {
                  "ticker": {
                    "type": "string",
                    "example": "NVDA"
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "add",
                      "remove"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "tickers": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid ticker/action.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "No active subscription.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "oauth2": {
        "type": "oauth2",
        "description": "See https://qmscreener.com/auth.md.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://qmscreener.com/oauth/authorize",
            "tokenUrl": "https://qmscreener.com/oauth/token",
            "refreshUrl": "https://qmscreener.com/oauth/token",
            "scopes": {
              "screener:read": "Read every screen's full results.",
              "watchlist:read": "Read the account's watchlist.",
              "watchlist:write": "Modify the account's watchlist.",
              "offline_access": "Receive a refresh token."
            }
          }
        }
      },
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "qm_session",
        "description": "Browser session cookie. Agents should use OAuth instead."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Structured error body returned by every failing endpoint.",
        "required": [
          "error",
          "code",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable machine-readable error code. Same value as `code`; kept for compatibility."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation."
          },
          "hint": {
            "type": "string",
            "description": "What the caller should do about it."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "Documentation for this error class."
          }
        }
      },
      "Screen": {
        "type": "object",
        "required": [
          "name",
          "methodology"
        ],
        "properties": {
          "name": {
            "type": "string",
            "enum": [
              "qullamaggie",
              "minervini_trend",
              "vcp",
              "gappers",
              "webster_recession",
              "webster_swing"
            ]
          },
          "methodology": {
            "type": "string"
          },
          "default_output": {
            "type": "string"
          },
          "filter_spec": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "default_sort": {
            "type": "object"
          },
          "compact_spec": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Preview": {
        "type": "object",
        "required": [
          "tier",
          "screens"
        ],
        "properties": {
          "tier": {
            "type": "string",
            "enum": [
              "public",
              "registered"
            ]
          },
          "screens": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "methodology": {
                  "type": "string"
                },
                "screening_date": {
                  "type": "string"
                },
                "passing_count": {
                  "type": "integer"
                },
                "signal": {
                  "type": "string"
                },
                "top": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Row"
                  }
                }
              }
            }
          }
        }
      },
      "Row": {
        "type": "object",
        "description": "One passing ticker with the metrics that decided it.",
        "properties": {
          "ticker": {
            "type": "string"
          },
          "company": {
            "type": "string"
          },
          "rs_3m_percentile": {
            "type": "number"
          },
          "adr_pct_20d": {
            "type": "number"
          },
          "dollar_vol_50d_m": {
            "type": "number"
          },
          "price_vs_sma50": {
            "type": "number"
          },
          "setup_tag": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "additionalProperties": true
      },
      "ScreenResults": {
        "type": "object",
        "properties": {
          "screening_date": {
            "type": "string"
          },
          "methodology": {
            "type": "string"
          },
          "passing_stocks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Row"
            }
          },
          "near_miss_stocks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Row"
            }
          },
          "value_stocks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Row"
            }
          }
        },
        "additionalProperties": true
      },
      "History": {
        "type": "object",
        "required": [
          "ticker",
          "bars"
        ],
        "properties": {
          "ticker": {
            "type": "string"
          },
          "through": {
            "type": "string",
            "description": "Last covered trading date."
          },
          "bars": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "t",
                "o",
                "h",
                "l",
                "c",
                "v"
              ],
              "properties": {
                "t": {
                  "type": "string",
                  "description": "Trading date, YYYY-MM-DD."
                },
                "o": {
                  "type": "number"
                },
                "h": {
                  "type": "number"
                },
                "l": {
                  "type": "number"
                },
                "c": {
                  "type": "number"
                },
                "v": {
                  "type": "number"
                }
              }
            }
          }
        }
      }
    }
  },
  "security": [
    {
      "oauth2": [
        "screener:read"
      ]
    },
    {
      "sessionCookie": []
    }
  ]
}
