Guides/Measure a Scan's Edge

    Guides

    Measure a Scan's Edge

    A scan that returns plausible-looking stocks every day isn't necessarily a good scan. The question is what those stocks did next. scanr.ai answers it for every saved scan: each time a stock enters the result, it's recorded as a simulated trade, and the trades are scored over holding periods from a day to a year.

    This guide reads those scores, drills into the trades behind them, and explains how to avoid fooling yourself with them.

    You'll need: a Power User or Business token, and a saved scan that has been running for a while.

    How the simulation works

    • A trade opens when a stock enters the scan's result, at the price when it was recorded.
    • It closes after a fixed time horizon: 1d, 3d, 5d, 10d, 1M, 3M, 6M, 1y, or at year end (ytd).
    • A stock that stays in the result doesn't open a new trade every run; there's a cooldown between re-entries.
    • These are simulated triggers, not broker fills: no slippage, commissions or position sizing.

    Trades are recorded as the scan runs, so the record builds up over time. A new scan reports insufficient_history: true until there's enough of it to judge.

    1. Read the scorecard

    POST /api/v2/scan/myscans/most-recent-scan-performances returns the latest statistics for your scans, grouped by horizon:

    python
    import osimport requests
    API = "https://scanr.ai/api/v2"HEADERS = {"Authorization": f"Bearer {os.environ['SCANR_API_TOKEN']}"}SCAN_ID = 918273  # the saved scan's leviathan_id
    perf = requests.post(    f"{API}/scan/myscans/most-recent-scan-performances",    headers=HEADERS,    json={"scan_ids": [SCAN_ID], "universe": "listed"},    timeout=60,).json()
    print(f"{'horizon':8} {'trades':>7} {'win':>7} {'avg':>7} {'expect':>7} {'sharpe':>7}")for horizon in ["1d", "5d", "10d", "1M", "3M"]:    for row in perf["results"].get(horizon) or []:        if row["insufficient_history"]:            print(f"{horizon:8} not enough history yet")            continue        print(            f"{horizon:8} {row['total_trades'] or 0:>7} "            f"{row['win_rate'] or 0:>7.1%} "            f"{row['avg_percent_return'] or 0:>7.2%} "            f"{row['expectancy'] or 0:>7.2%} "            f"{row['sharpe_ratio'] or 0:>7.2f}"        )

    scan_ids: null returns every saved scan you own, which makes it easy to rank them against each other. universe limits the trades to exchange-listed stocks (listed), OTC (otc), or both (all, the default).

    Percentages come back as fractions: a win_rate of 0.58 is 58%, an avg_percent_return of 0.0784 is 7.84%. Sharpe, Sortino and Calmar are plain ratios. The fields worth reading first:

    FieldMeaning
    total_tradesHow many trades the numbers rest on. Read this before anything else.
    win_rateThe share of trades that ended in profit.
    avg_percent_returnThe average return per trade over the horizon.
    expectancyWhat a trade is worth on average: win rate × average win − loss rate × average loss.
    sharpe_ratio, sortino_ratioReturn per unit of risk (all volatility, or downside only).
    avg_max_drawdownHow far trades typically went against you before the horizon ended.
    median_mfe, median_maeThe typical best and worst point of a trade: how much room it gave you.
    top_tickersThe tickers that traded most often.

    2. Look at the trades

    Aggregates hide a lot. POST /api/v2/scan/myscans/scans-for-ticker tells you which of your scans have traded a ticker, and POST /api/v2/scan/myscans/ticker-trade-outcomes lists every simulated trade for one ticker in one scan:

    python
    trades = requests.post(    f"{API}/scan/myscans/ticker-trade-outcomes",    headers=HEADERS,    json={"scan_id": SCAN_ID, "ticker": "NVDA", "time_horizon": "1M"},    timeout=60,).json()
    for t in trades["results"] or []:    status = "closed" if t["DidExit"] else "open"    print(t["ActualEntryDate"][:10], f"{t['PercentageReturn']:+.2%}", status)

    Each trade has its entry and exit dates, PercentageReturn (a fraction), DidExit (whether the horizon has elapsed), BarsHeld, and the best and worst points along the way (MaxProfitSinceEntryPercentage, MaxLossSinceEntryPercentage, MaxDrawdown). Entry and exit prices are not included in API responses; use the dates to look them up on a chart.

    3. Judge it honestly

    • Count the trades. Twenty trades can show a 70% win rate by chance. Treat anything under a hundred trades as a hint, not a result.
    • Pick the horizon before you look. Nine horizons give nine chances for one of them to look good. Decide how long you intend to hold, then read that row.
    • Compare with doing nothing. A trend-following scan in a rising market will look good at every horizon. Save a deliberately dumb control scan (for example, every stock above $10 with RS above 50) and compare the two.
    • Watch the drawdowns. A strong avg_percent_return with a deep avg_max_drawdown is a strategy you probably won't hold through.
    • Change one thing at a time. Save variants as separate scans (a different RSI threshold, an extra liquidity filter) and let them run side by side. The one with more trades and a similar edge is usually the better choice.
    • Expect decay. Edges fade as markets change. Re-check scans you rely on every few months.

    Next