prizepicksapi

Backtesting

Backtest with PrizePicks line data, honestly.

Recorded history can support a backtest only if you respect what it is: dated observations from this service, stored when an offer changed, with no results attached. This guide covers the reconstruction step and the limits to report.

Six limits to state up front

LimitConsequence for a backtest
Window starts when observation startedNo sample before the earliest retained entry. Check the measured window on the historical data guide.
History is change-onlyAn entry is stored when the normalized offer changes, not on every collection. Gaps between entries are not proof of an unchanged line.
Collection can be stale or partialMissed intermediate moves cannot be recovered. A disappearance in a partial collection may not be a removal.
first_seen_at is not an opening timeOpening-line strategies need upstream timestamps you may not have.
Base line onlypromotions_modeled is false; the line is not necessarily what a given user could select.
No results or payoutsOutcomes must come from another source; null payout multipliers stay unknown.

Reconstruct the line as of a cutoff

For each simulated decision, use only what was known at the decision time. With change-only history, the state at a cutoff is the latest entry observed at or before that cutoff. If none exists, the offer was unknown to this service at that time and must be excluded, not filled in from a later entry.

from datetime import datetime

def parse(ts: str) -> datetime:
    return datetime.fromisoformat(ts.replace("Z", "+00:00"))

def line_as_of(history: list[dict], cutoff: datetime):
    """history: entries from /api/v1/projections/{id}/history (all pages)."""
    known = [h for h in history if parse(h["observed_at"]) <= cutoff]
    if not known:
        return None  # not yet observed: exclude, do not backfill
    latest = max(known, key=lambda h: (parse(h["observed_at"]), int(h["id"])))
    if latest["status"] != "active":
        return None  # offer was recorded unavailable at the cutoff
    return {"line": latest["line"], "selections": latest["selections"],
            "observed_at": latest["observed_at"]}

Read every history page before reconstructing; a single page may omit the latest entries. Keep the observed_at you used in the output row so a reviewer can see how stale the input was at decision time.

Avoid look-ahead leakage

  • Never select a sample using fields known only later, such as last_seen_at from a current projection read.
  • Do not filter to offers that still exist today; that removes everything that went unavailable.
  • Join results by event and market, not by player name alone. One player can have several markets and periods on the same day.
  • Freeze the dataset with its export time and source_status so the backtest can be re-run on the same inputs.

Report coverage, not just returns

A useful backtest report states the observation window, the number of projections and history entries used, how many decisions were excluded for missing data, and the share of collections in the window that were partial or stale if you tracked them. Those numbers tell a reader whether a result reflects the strategy or the sampling.

Frequently asked questions

Does the API provide game results or graded outcomes?

No. results_available is false and no settlement data is exposed. Join a separately validated results source and document how markets map to it.

Can I treat the last stored line as the closing line?

Only with a caveat. It is the last state this service recorded before your cutoff, which may differ from the final upstream line if a change was not observed.

References

Current API documentation ↗Machine-readable OpenAPI contract ↗

PrizePicks API is an independent data service. It is not affiliated with or endorsed by PrizePicks. Observations, completeness, and freshness are separate claims; consult source status before relying on a stored line.