Guides/Track Theme Rotation

    Guides

    Track Theme Rotation

    Money rotates between groups of stocks: miners one month, networking hardware the next. scanr.ai's themes are those groups, found from how stocks actually trade together (their price correlation) rather than from industry codes, so a theme like silver miners holds the stocks that really move with silver.

    This guide finds the themes gaining strength, then scans inside the best one for its leaders.

    You'll need: a Business token for the theme endpoints. The scan in step 3 runs on any plan.

    1. List the themes

    GET /api/v2/stocks/themes returns every theme with its members:

    python
    import osimport requests
    API = "https://scanr.ai/api/v2"HEADERS = {"Authorization": f"Bearer {os.environ['SCANR_API_TOKEN']}"}
    catalog = requests.get(f"{API}/stocks/themes", headers=HEADERS, timeout=60).json()themes = [t for t in catalog["themes"] if t["theme_band"] != "residual"]for theme in themes[:10]:    print(theme.get("theme_key"), "|", theme["label"], "|", theme["num_tickers"], "stocks")

    Each theme has a stable theme_key (use it in scans), a label and description, its member tickers with each member's tier (1 for core members, higher for looser ones), and avg_intra_correlation, how tightly the members move together.

    theme_band says what kind of group it is: canonical themes have a recognized name, unnamed ones are real correlated groups still waiting for one, and residual groups are leftovers bucketed by size or float rather than a shared story. Skip residual for rotation work.

    Memberships are recalculated weekly.

    2. Rank themes by strength

    GET /api/v2/stocks/themes/performance/latest scores every theme for the current session:

    FieldMeaning
    performanceThe theme's return for the session, across its members.
    avg_rs_scoreThe average relative strength (1–99) of the theme's members.
    rs_slope_20d, rs_slope_r2The trend of that average over the last 20 sessions, and how steady the trend is (R²).
    avg_relative_volumeHow heavily the members are trading compared with normal.
    tickersEach member's daily_return, relative_volume and dollar_volume.

    Rotation shows up as rising strength more than high strength. Rank by the RS slope, keeping only steady trends:

    python
    latest = requests.get(f"{API}/stocks/themes/performance/latest", headers=HEADERS, timeout=60).json()
    rising = sorted(    (        t for t in latest["themes"] or []        if t["theme_band"] != "residual"        and t.get("rs_slope_20d") is not None        and (t.get("rs_slope_r2") or 0) >= 0.5    ),    key=lambda t: t["rs_slope_20d"],    reverse=True,)for t in rising[:10]:    print(f"{t['label']:40} RS {t['avg_rs_score']:5.1f}  slope {t['rs_slope_20d']:+.2f}")

    For your own measures, pull the time series from GET /api/v2/stocks/themes/performance/history?days=90: one entry per session (perf_date), each with every theme's figures. GET /api/v2/stocks/themes/performance/by-date?date=2026-09-15 returns a single past session.

    3. Scan inside the theme

    Universe.Theme restricts any scan to a theme's members. This finds the members of the top rising theme that are themselves leaders, above their 50-day average with RS of 70 or more.

    Get the leadership part of the payload the usual way: send "relative strength of 70 or more, and price above the 50-day moving average" to /scan/natural, or export a scan you already use from the scanr.ai scanner (Filters → Export filters). Save it as theme-leaders.json. The theme is the part that changes from week to week, so the code sets it. Written out, with the theme filled in, the payload is:

    json
    {  "Universe": {    "Exchange": ["nasdaq", "nyse", "amex", "arca", "bats", "bx"],    "Type": ["cs", "os", "adrc"],    "Theme": ["silver-miners"],    "ThemeMaxTier": 2  },  "All": [    {      "Indicator": {        "RelativeStrengthMkt": [          { "Returns": { "Want": { "Value": { "GreaterThanOrEqual": 70 } } } }        ]      },      "Technical": {        "SMA": [          {            "Timeframe": "1d",            "Inputs": ["bar_close"],            "Params": { "Period": { "Value": 50 } },            "Returns": { "Want": { "SMA": { "Price": { "Comparison": { "LessThan": true } } } } }          }        ]      }    }  ],  "Sort": { "field": "market_cap", "direction": "desc" }}

    In code, fill the theme from step 2 and run it:

    python
    import json
    with open("theme-leaders.json") as f:    payload = json.load(f)
    payload["Universe"]["Theme"] = [rising[0]["theme_key"]]result = requests.post(f"{API}/scan", headers=HEADERS, json={"instructions": payload}, timeout=60).json()print(rising[0]["label"], "leaders:", result["tickers"])

    ThemeMaxTier sets how far from the theme's core to look: 1 core members only, 2 core and satellite members (the default), 3 every member. Theme accepts several keys, which are alternatives: a stock in any of them qualifies.

    GET /api/v2/scan/universe-themes lists the scannable theme keys with member counts, and works on any plan. A key that isn't in it matches nothing, without an error.

    Putting it together

    A weekly rotation report is these three steps in a loop: rank themes by RS slope, take the top three, scan each for leaders with the same theme-leaders.json, and report the leaders alongside their theme's figures. It costs one request for the performance data and one per theme scanned, and no AI credits after the payload is first generated.

    Next