Guide

Build a fantasy trade analyzer with rest-of-season value over replacement

Quick answer: Sum each player's /projections/{week} from the upcoming week to the end of your fantasy season, add a regressed slice of his /deviations average, and subtract what the last starter at his position scores over the same weeks. Floor the result at zero and add up each side. In the worked trade below, the side giving up two players loses 79.6 raw points and still comes out 57.9 points ahead, because one of the two players it gives up is worth no more than a waiver pickup.

Endpoints
/projections, /deviations, /snaps, /news
Requests
~21 per trade
Plan
Basic
Language
Python

Why can't I just add up each side's points?

Because a trade changes how many roster spots each manager has to fill. In a 2-for-1, the manager who gets one player also gets an empty spot, and he fills it with the best player on the waiver wire. That pickup scores points too. Add up raw projections and you count the second player at full price while pricing the open roster spot at nothing, so every 2-for-1 looks good for the side getting two.

The fix is the same idea behind our draft value tool: value each player by what he scores above a replacement-level player at his position. In a 12-team league that line is roughly the 30th running back, the 36th wide receiver and the 12th quarterback and tight end. A player on that line is worth zero in a trade, because the wire will give you the same points for free. A player below it is also worth zero, not a negative number: you would simply drop him.

That floor is the whole trick. It is what stops two bench players from adding up to a star, and it is why the question to ask about any offer is not "who gets more points" but "who gets more points he couldn't have had anyway."

What goes into a player's trade value?

Three numbers are scored and three signals are printed beside them. A trade is a question about the rest of the season, so everything is measured from the upcoming week to the last week your league plays.

The pieces of the model and where each one comes from
PieceSourceHow it's used
Rest-of-season (ROS) points/projections/{week}, summed over the remaining weeksThe base value
Formavg_deviation from /deviationsAdded to ROS after regression
Replacement levelThe last starter at the position, from the same projectionsSubtracted, floored at zero
Roletrend_delta from /snapsFlag, not scored
Healthinjury_status and /news?impact=injuryFlag, not scored

Form is regressed on purpose. avg_deviation is the average gap between a player's actual and projected points this season. Five weeks is a small sample, so the script carries forward only n / (n + 8) of it, where n is the weeks analyzed. A player beating his number by 5.0 a game through five weeks gets credit for 1.9 a game from here on. By Week 12 that share is a little over half.

Role and health stay as flags on purpose. Turning "Questionable" into a points penalty means inventing a number. Printing the status and the newest headline next to the value costs you ten seconds of reading and keeps the arithmetic honest. A snap-share swing of five points or more in either direction is printed the same way; our snap share guide covers how to read one.

Which endpoints does the analyzer call?

  • GET /projections/{week}?scoring=ppr: with no position filter, one call returns every player for that week. Each row carries projection_source: "weekly" for a published weekly projection, or "season_prorated" (the season-long projection divided by 17) for weeks that haven't been published yet. Summing week by week means published numbers replace the estimates as they arrive.
  • GET /deviations/{player_id}: avg_deviation and weeks_analyzed for the current season.
  • GET /snaps/{player_id}: trend_delta, the recent four-week snap share minus the season average.
  • GET /news/{player_id}?impact=injury&limit=1: the newest injury headline. With that filter the API looks back 10 days by default.

Request budget: one call per remaining week plus three per player. The 2-for-1 below, run before Week 6 with a Week 17 finish, is 12 + 9 = 21 requests. Basic allows 30 a minute and 5,000 a month, so that is about 230 trades a month. The free plan's 10 requests a minute is too tight to run this in one pass, which is why the facts box says Basic.

How do I build the analyzer in Python?

Set GRIDIRON_API_KEY, edit GIVE, GET and WEEK, and run it. Player IDs are First Last#POS; the script URL-encodes them for you.

import os, requests
from urllib.parse import quote

API = "https://api.gridirondata.com/api/v1"
H = {"x-api-key": os.environ.get("GRIDIRON_API_KEY", "YOUR_KEY")}

GIVE = ["Chuba Hubbard#RB", "DK Metcalf#WR"]
GET = ["CeeDee Lamb#WR"]
WEEK, LAST_WEEK = 6, 17          # upcoming week -> last week of your fantasy season
SCORING = "ppr"                  # or "half" / "standard"
MISSED = {}                      # games you expect a player to miss, e.g. {"CeeDee Lamb#WR": 2}

REPLACEMENT_RANK = {"QB": 12, "RB": 30, "WR": 36, "TE": 12}   # last starter, 12-team league
SHRINK = 8                       # weeks of evidence before form counts for half
WEEKS_LEFT = LAST_WEEK - WEEK + 1

def get(path, **params):
    r = requests.get(f"{API}{path}", headers=H, params=params, timeout=30)
    if r.status_code == 404:     # no games or no snaps yet for this player
        return {}
    r.raise_for_status()
    return r.json()

# 1) Rest-of-season projection for every player: sum the remaining weeks.
players = {}
for week in range(WEEK, LAST_WEEK + 1):
    for p in get(f"/projections/{week}", scoring=SCORING)["projections"]:
        row = players.setdefault(p["player_id"], {"pos": p["position"], "status": p["injury_status"], "ros": 0.0})
        row["ros"] += float(p["projected_points"])

# 2) Replacement level: what the last starter at each position scores over the same weeks.
replacement = {}
for pos, rank in REPLACEMENT_RANK.items():
    scored = sorted((p["ros"] for p in players.values() if p["pos"] == pos), reverse=True)
    replacement[pos] = scored[min(rank, len(scored)) - 1]

# 3) Value one player: ROS + regressed form - replacement, with flags on the side.
def evaluate(pid):
    row, enc = players[pid], quote(pid, safe="")
    games = max(WEEKS_LEFT - MISSED.get(pid, 0), 0)
    ros = row["ros"] * games / WEEKS_LEFT

    dev = get(f"/deviations/{enc}")
    n = dev.get("weeks_analyzed", 0)
    form = dev.get("avg_deviation", 0) * n / (n + SHRINK) * games

    flags = [] if row["status"] in ("Healthy", "Unknown") else [row["status"]]
    delta = get(f"/snaps/{enc}").get("trend_delta")
    if delta is not None and abs(delta) >= 5:
        flags.append(f"snaps {delta:+.0f}")
    news = get(f"/news/{enc}", impact="injury", limit=1).get("news", [])
    if news:
        flags.append(f"news: {news[0]['headline'][:60]}")

    value = max(ros + form - replacement[row["pos"]], 0.0)   # below replacement = worth 0
    return pid, ros, form, value, flags

def show(label, ids):
    print(f"\n{label}")
    total = 0.0
    for pid, ros, form, value, flags in map(evaluate, ids):
        total += value
        print(f"  {pid:26} ROS {ros:6.1f}  form {form:+6.1f}  value {value:6.1f}")
        for flag in flags:
            print(f"      ! {flag}")
    print(f"  {'TOTAL':26} {'':24} value {total:6.1f}")
    return total

print("Replacement level: " + "  ".join(f"{pos} {pts:.1f}" for pos, pts in replacement.items()))
give, got = show("YOU GIVE", GIVE), show("YOU GET", GET)
net = got - give
verdict = "roughly even" if abs(net) < 10 else ("favors you" if net > 0 else "favors them")
print(f"\nNet: {net:+.1f} points over replacement -> {verdict}")

The value line is the whole model: max(ROS + form − replacement, 0). Everything above it is fetching, and everything below it is printing. A command-line version with --give, --get, --scoring, --teams and --out flags is in the examples as trade_analyzer.py.

What does the output look like?

This is a real run made on October 13, 2026, before Week 6, with data through Week 5. You give Chuba Hubbard and DK Metcalf and get CeeDee Lamb, in a 12-team PPR league that plays through Week 17:

Replacement level: QB 211.1  RB 130.8  WR 130.5  TE 120.6

YOU GIVE
  Chuba Hubbard#RB           ROS  136.9  form  +24.6  value   30.8
  DK Metcalf#WR              ROS  139.0  form   -8.7  value    0.0
  TOTAL                                               value   30.8

YOU GET
  CeeDee Lamb#WR             ROS  196.3  form  +22.8  value   88.6
      ! Questionable
      ! news: Good news for CeeDee Lamb
  TOTAL                                               value   88.6

Net: +57.9 points over replacement -> favors you

How to read it:

  • Raw points call it a loss. Hubbard and Metcalf project for 275.9 ROS points between them; Lamb projects for 196.3. You are giving up 79.6 points on paper.
  • Value calls it a win. Metcalf sits 8.5 points above the WR36 line on projection and drops just under it once his negative form is counted, so he adds nothing the waiver wire wouldn't. Lamb is 88.6 points clear of that same line. Hubbard is the only real cost, at 30.8.
  • The flags say read before you accept. Lamb is Questionable and has an injury headline from the last 10 days. This one reads as good news, but the script can't tell; you can.

Your numbers will differ on the day you run it, because every remaining week is re-pulled each time and the form adjustment grows as the season supplies more weeks.

How should I tune it for my league?

  • League size and lineup: REPLACEMENT_RANK assumes a 12-team league. For 10 teams use roughly QB10, RB25, WR30, TE10. In superflex, push QB to 24 and watch quarterbacks jump in value.
  • Scoring: set SCORING to "half" or "standard". The same trade in half-PPR still favors the Lamb side, but by 17.8 points instead of 57.9, because receivers lose more than backs when a catch is worth less.
  • How much to believe form: raise SHRINK to trust the projections more, lower it to trust this season's results more.
  • Missed games: put expected absences in MISSED. A player you expect to miss three of twelve weeks keeps three quarters of his projection and form.
  • Playoffs only: set WEEK and LAST_WEEK to your playoff weeks to value a deal for the stretch run alone.

What can't this analyzer tell me?

It doesn't know your roster. Value over replacement measures a player against the league's waiver wire, not against your own bench, so a running back is worth more to a team starting a backup than this number says. It doesn't skip bye weeks in the prorated weeks, so a player who has already had his bye is slightly undervalued next to one who hasn't. Prorated weeks also don't know that a player is on injured reserve or when he returns; that is what MISSED is for. And it prices this season only: no keepers, no draft picks, no age.

Treat a gap under about 10 points as even. The script does, and at that size the regression constant and the replacement rank you chose matter more than the players do.

Frequently asked questions

Why not just add up each side's projected points?

Because the side that gets fewer players also gets an open roster spot and fills it from the waiver wire. Raw points ignore that. Value over replacement counts only what each player scores above the last starter at his position, so a 2-for-1 is judged on what it adds to a lineup.

How many API requests does one trade cost?

One /projections call per remaining week plus three calls per player (deviations, snaps and news). A 2-for-1 before Week 6 is 12 + 9 = 21 requests. That fits the Basic plan's 30 requests a minute; the free plan's 10 a minute is too tight to run it in one pass.

Does it work for half-PPR and standard leagues?

Yes. Set SCORING to half or standard and the projections and the replacement level both follow. The form adjustment comes from /deviations, which is measured against the PPR projection.

How does it handle an injured player?

It prints his injury status and the newest injury headline as flags, and it does not guess a discount. If you expect him to miss games, put the number in MISSED and his projection and form are scaled to the games he plays.

Can I use it for a dynasty or keeper trade?

Not as written. It values the rest of this season only. For a dynasty trade you would add future seasons, age and draft picks, none of which this script prices.