Guides/Run an End-of-Day Screen

    Guides

    Run an End-of-Day Screen

    The most common use of the API: once the market closes, run your screen, rank the matches, and save a list you'll act on tomorrow. This guide builds that as one script and schedules it.

    You'll need: a token (any plan for the scan; Power User or higher for the relative-strength column), and Python 3.9+ with requests, or Node 18+.

    1. Choose the screen

    The script runs whatever payload is in screen.json. Keeping the screen in a file, not in the script, lets you change it without touching code. Get it either way:

    • Export it from the scanner. Open the scan in the scanr.ai scanner, click Filters → Export filters, then Download JSON, and save the file as screen.json. This is the easiest way to run a screen you already watch in the app.
    • Describe it. Send the screen to /scan/natural, read the reasoning it returns, and save generated_payload as screen.json:
    bash
    curl -s https://scanr.ai/api/v2/scan/natural \  -H "Authorization: Bearer $SCANR_API_TOKEN" \  -H "Content-Type: application/json" \  -d '{"query": "price above $10, average daily dollar volume over the last 50 days above $20M, price above the 50-day moving average, the 50-day above the 200-day, and relative strength of 75 or more"}' \  | jq '.generated_payload' > screen.json

    Either way you generate or export it once. The nightly run then costs one request and no AI credits.

    This guide uses a classic trend-following watchlist: stocks above $10 trading at least $20M a day on average, above their 50-day moving average, with the 50-day above the 200-day and relative strength in the top quarter of the market. Written out, screen.json is the following. The three SMA rows are three separate conditions, and all of them must hold.

    json
    {  "Universe": {    "Exchange": ["nasdaq", "nyse", "amex", "arca", "bats", "bx"],    "Type": ["cs", "os", "adrc"]  },  "All": [    {      "Stock": {        "PrevDayClose": [          { "State": { "Value": { "Lower": { "Operator": "gte", "Value": 10 } } } }        ]      },      "Technical": {        "SMA": [          {            "Timeframe": "1d",            "Inputs": ["bar_close"],            "Params": { "Period": { "Value": 50 } },            "Returns": { "Want": { "SMA": { "Price": { "Comparison": { "LessThan": true } } } } }          },          {            "Timeframe": "1d",            "Inputs": ["bar_close"],            "Params": { "Period": { "Value": 50 } },            "Returns": {              "Want": {                "SMA": {                  "Study": {                    "GreaterThan": {                      "SMA": [                        {                          "Timeframe": "1d",                          "Inputs": ["bar_close"],                          "Params": { "Period": { "Value": 200 } },                          "Returns": { "Want": { "SMA": { "PlotChild": true } } }                        }                      ]                    }                  }                }              }            }          },          {            "Timeframe": "1d",            "Inputs": ["bar_dollar_volume"],            "Params": { "Period": { "Value": 50 } },            "Returns": { "Want": { "SMA": { "Value": { "GreaterThan": 20000000 } } } }          }        ]      },      "Indicator": {        "RelativeStrengthMkt": [          { "Returns": { "Want": { "Value": { "GreaterThanOrEqual": 75 } } } }        ]      }    }  ],  "Sort": { "field": "market_cap", "direction": "desc" }}

    2. Write the script

    The script checks the market is closed (so daily bars are settled), runs the scan, adds each match's relative-strength score, and writes a dated CSV.

    python
    """End-of-day scan: run screen.json after the close and save the matches."""import csvimport datetime as dtimport jsonimport osimport sysimport time
    import requests
    API = "https://scanr.ai/api/v2"HEADERS = {"Authorization": f"Bearer {os.environ['SCANR_API_TOKEN']}"}
    
    def call(method, path, **kwargs):    """One API call, retrying 429s and 5xx with backoff."""    for attempt in range(4):        response = requests.request(method, API + path, headers=HEADERS, timeout=120, **kwargs)        if response.status_code not in (429, 500, 502, 503, 504) or attempt == 3:            response.raise_for_status()            return response.json()        time.sleep(60 if response.status_code == 429 else 2**attempt)
    
    def market_is_closed():    status = call("GET", "/misc/market-status")["status"]    return status.get("market") == "closed"
    
    def latest_rs(tickers):    """Latest market RS (1-99) per ticker. Needs a Power User or Business token."""    since = int((time.time() - 5 * 86400) * 1000)  # the last few sessions, in ms    body = {"tickers": tickers, "from": since, "limit": 500}    latest = {}    while True:        page = call("POST", "/stocks/get-stock-indicators", json=body)        for row in page.get("results") or []:            seen = latest.get(row["ticker"])            if seen is None or row["date"] > seen["date"]:                latest[row["ticker"]] = row        if page.get("is_last_page", True):            break        body["cursor_date"] = page["next_cursor_date"]        body["cursor_ticker"] = page["next_cursor_ticker"]    return {ticker: row.get("rs_score") for ticker, row in latest.items()}
    
    def main():    if not market_is_closed() and "--force" not in sys.argv:        sys.exit("Market is not closed yet; daily bars aren't settled. Use --force to run anyway.")
        with open("screen.json") as f:        payload = json.load(f)  # an exported or generated payload
        result = call("POST", "/scan", json={"instructions": payload})    tickers = result["tickers"] or []    print(f"{result['count']} matches")
        try:        rs = latest_rs(tickers) if tickers else {}    except requests.HTTPError as error:        if error.response.status_code != 403:            raise        rs = {}  # Starter plan: no RS column
        today = dt.date.today().isoformat()    with open(f"screen-{today}.csv", "w", newline="") as f:        writer = csv.writer(f)        writer.writerow(["date", "rank", "ticker", "rs_score"])        for rank, ticker in enumerate(tickers, start=1):            writer.writerow([today, rank, ticker, rs.get(ticker, "")])
    
    if __name__ == "__main__":    main()

    The script makes three kinds of request (one scan, one market-status check, and a relative-strength call per page of 500 rows), so it fits comfortably in any plan's limits.

    3. Schedule it

    Run it on weekdays at 16:30 New York time, which leaves a margin after the 16:00 close. On a market holiday the market is closed all day, so the script runs and returns the previous session's list.

    bash
    # crontab -e   (CRON_TZ is supported by cronie and most modern crons)CRON_TZ=America/New_York30 16 * * 1-5  cd /path/to/screen && SCANR_API_TOKEN=scanr_... python3 eod.py >> eod.log 2>&1

    GitHub's scheduler runs in UTC and can start late under load. During winter (EST) 20:30 UTC is 15:30 in New York, before the close, and the script exits without scanning. Use "30 21 * * 1-5" from November to March, or run it hourly after 20:00 UTC and let the market-status check decide.

    4. Build on it

    • Diff against yesterday. Load the previous CSV and report only new tickers: the stocks that entered the screen today are usually the actionable ones.
    • Let scanr.ai do the scheduling. Save the scan instead, and it runs in the background; read what entered it from POST /api/v2/scan/paginated-scan-events.
    • Judge the screen. Once it's saved, its performance tells you whether the stocks it picks actually go on to outperform.

    Next