|
| 1 | +--- |
| 2 | +description: "GET /api/v1/historical/clv — Closing Line Value analysis across sportsbooks. Compare your closing prices against Pinnacle's no-vig probability to measure edge. Sharp tier required." |
| 3 | +--- |
| 4 | + |
| 5 | +import { Callout, Tabs } from 'nextra/components' |
| 6 | + |
| 7 | +# Closing Line Value (CLV) |
| 8 | + |
| 9 | +Analyze how well a sportsbook's closing odds compare to Pinnacle's devigged (no-vig) probability. Positive CLV means the book closed looser than Pinnacle — a historical proxy for edge. |
| 10 | + |
| 11 | +``` |
| 12 | +GET /api/v1/historical/clv |
| 13 | +``` |
| 14 | + |
| 15 | +<Callout type="warning"> |
| 16 | +**Sharp tier or higher required.** Pro and lower tiers receive a `403 tier_restricted` error. This endpoint is gated by the `closing_lines` feature. |
| 17 | +</Callout> |
| 18 | + |
| 19 | +<Callout type="info"> |
| 20 | +**Phase 1 data window:** Closing lines are captured in real-time as games go live. The Valkey store retains data for **30 days**. Historical data beyond 30 days is not yet available. |
| 21 | +</Callout> |
| 22 | + |
| 23 | +## How Closing Lines Are Captured |
| 24 | + |
| 25 | +When a game transitions from pre-match to live, the server captures each book's last pre-match odds as that event's closing line. The closing price is devigged using the **Power method** (same as EV calculation) to produce a no-vig probability per selection. |
| 26 | + |
| 27 | +CLV is then computed as: |
| 28 | + |
| 29 | +``` |
| 30 | +CLV% = (pin_no_vig_prob - book_no_vig_prob) / book_no_vig_prob × 100 |
| 31 | +``` |
| 32 | + |
| 33 | +Where `pin_no_vig_prob` is Pinnacle's devigged probability (the sharp reference) and `book_no_vig_prob` is the soft book's devigged probability. A positive CLV% means the soft book closed at a price more favorable than the Pinnacle fair line. |
| 34 | + |
| 35 | +## Authentication |
| 36 | + |
| 37 | +Requires API key. **Sharp tier or higher required** (`closing_lines` feature). |
| 38 | + |
| 39 | +## Query Parameters |
| 40 | + |
| 41 | +| Parameter | Type | Default | Description | |
| 42 | +|-----------|------|---------|-------------| |
| 43 | +| `from` | string | 7 days ago | Start date (`YYYY-MM-DD` or ISO 8601). Clamped to 30-day window. | |
| 44 | +| `to` | string | today | End date (`YYYY-MM-DD` or ISO 8601). | |
| 45 | +| `group_by` | string | `sportsbook` | Aggregation dimension. One of: `sportsbook`, `sport`, `league`, `market`, `day`. | |
| 46 | +| `sport` | string | all | Filter by sport(s), comma-separated (e.g. `basketball,football`). | |
| 47 | +| `league` | string | all | Filter by league(s), comma-separated (e.g. `nba,nfl`). | |
| 48 | +| `sportsbook` | string | all | Filter by sportsbook(s), comma-separated. | |
| 49 | + |
| 50 | +### Date Window |
| 51 | + |
| 52 | +Sharp tier allows up to **30 days** of history (the current retention window for closing line data). Requesting dates outside this window is clamped silently to the earliest available date. |
| 53 | + |
| 54 | +## Example Requests |
| 55 | + |
| 56 | +<Tabs items={['cURL', 'JavaScript', 'Python']}> |
| 57 | + <Tabs.Tab> |
| 58 | +```bash |
| 59 | +# CLV by sportsbook for the past 7 days |
| 60 | +curl -X GET "https://api.sharpapi.io/api/v1/historical/clv?from=2026-04-10&to=2026-04-17&group_by=sportsbook" \ |
| 61 | + -H "X-API-Key: YOUR_API_KEY" |
| 62 | + |
| 63 | +# CLV grouped by sport, NBA only |
| 64 | +curl -X GET "https://api.sharpapi.io/api/v1/historical/clv?league=nba&group_by=sport" \ |
| 65 | + -H "X-API-Key: YOUR_API_KEY" |
| 66 | + |
| 67 | +# CLV grouped by day for DraftKings |
| 68 | +curl -X GET "https://api.sharpapi.io/api/v1/historical/clv?sportsbook=draftkings&group_by=day" \ |
| 69 | + -H "X-API-Key: YOUR_API_KEY" |
| 70 | +``` |
| 71 | + </Tabs.Tab> |
| 72 | + <Tabs.Tab> |
| 73 | +```javascript |
| 74 | +const params = new URLSearchParams({ |
| 75 | + from: '2026-04-10', |
| 76 | + to: '2026-04-17', |
| 77 | + group_by: 'sportsbook', |
| 78 | +}); |
| 79 | +const response = await fetch( |
| 80 | + `https://api.sharpapi.io/api/v1/historical/clv?${params}`, |
| 81 | + { headers: { 'X-API-Key': 'YOUR_API_KEY' } } |
| 82 | +); |
| 83 | +const { data, meta } = await response.json(); |
| 84 | +for (const group of data.groups) { |
| 85 | + console.log(`${group.group_key}: avg CLV ${group.avg_clv_percent.toFixed(2)}% (${group.samples} samples)`); |
| 86 | +} |
| 87 | +``` |
| 88 | + </Tabs.Tab> |
| 89 | + <Tabs.Tab> |
| 90 | +```python |
| 91 | +import requests |
| 92 | + |
| 93 | +response = requests.get( |
| 94 | + 'https://api.sharpapi.io/api/v1/historical/clv', |
| 95 | + params={ |
| 96 | + 'from': '2026-04-10', |
| 97 | + 'to': '2026-04-17', |
| 98 | + 'group_by': 'sportsbook', |
| 99 | + }, |
| 100 | + headers={'X-API-Key': 'YOUR_API_KEY'} |
| 101 | +) |
| 102 | +data = response.json() |
| 103 | +for group in data['data']['groups']: |
| 104 | + print( |
| 105 | + f"{group['group_key']}: " |
| 106 | + f"avg CLV {group['avg_clv_percent']:.2f}% " |
| 107 | + f"({group['samples']} samples)" |
| 108 | + ) |
| 109 | +``` |
| 110 | + </Tabs.Tab> |
| 111 | +</Tabs> |
| 112 | + |
| 113 | +## Response |
| 114 | + |
| 115 | +### Success (200) |
| 116 | + |
| 117 | +```json |
| 118 | +{ |
| 119 | + "success": true, |
| 120 | + "data": { |
| 121 | + "group_by": "sportsbook", |
| 122 | + "groups": [ |
| 123 | + { |
| 124 | + "group_key": "draftkings", |
| 125 | + "samples": 312, |
| 126 | + "avg_clv_percent": 1.42, |
| 127 | + "avg_abs_clv_percent": 2.18, |
| 128 | + "distinct_books": 1, |
| 129 | + "distinct_sports": 4 |
| 130 | + }, |
| 131 | + { |
| 132 | + "group_key": "fanduel", |
| 133 | + "samples": 289, |
| 134 | + "avg_clv_percent": 0.87, |
| 135 | + "avg_abs_clv_percent": 1.95, |
| 136 | + "distinct_books": 1, |
| 137 | + "distinct_sports": 4 |
| 138 | + }, |
| 139 | + { |
| 140 | + "group_key": "betmgm", |
| 141 | + "samples": 201, |
| 142 | + "avg_clv_percent": -0.21, |
| 143 | + "avg_abs_clv_percent": 1.74, |
| 144 | + "distinct_books": 1, |
| 145 | + "distinct_sports": 3 |
| 146 | + } |
| 147 | + ], |
| 148 | + "date_range": { |
| 149 | + "from": "2026-04-10T00:00:00.000", |
| 150 | + "to": "2026-04-17T00:00:00.000" |
| 151 | + }, |
| 152 | + "total_events": 58, |
| 153 | + "comparable_events": 51, |
| 154 | + "sharp_reference": "pinnacle" |
| 155 | + }, |
| 156 | + "meta": { |
| 157 | + "source": "valkey:closing_line", |
| 158 | + "tier_window_days": 30, |
| 159 | + "phase": "1", |
| 160 | + "filters": { |
| 161 | + "sport": null, |
| 162 | + "league": null, |
| 163 | + "sportsbook": null, |
| 164 | + "group_by": "sportsbook" |
| 165 | + }, |
| 166 | + "updated_at": "2026-04-17T20:00:00.000000000Z" |
| 167 | + } |
| 168 | +} |
| 169 | +``` |
| 170 | + |
| 171 | +**Empty result (no closing line data yet):** |
| 172 | + |
| 173 | +```json |
| 174 | +{ |
| 175 | + "success": true, |
| 176 | + "data": { |
| 177 | + "group_by": "sportsbook", |
| 178 | + "groups": [], |
| 179 | + "date_range": { "from": "...", "to": "..." }, |
| 180 | + "total_events": 0, |
| 181 | + "comparable_events": 0, |
| 182 | + "sharp_reference": "pinnacle" |
| 183 | + }, |
| 184 | + "meta": { "source": "valkey:closing_line", "phase": "1", ... } |
| 185 | +} |
| 186 | +``` |
| 187 | + |
| 188 | +An empty `groups` array is a valid response when no closing line data has been captured for the requested date range — not an error. |
| 189 | + |
| 190 | +### Error Responses |
| 191 | + |
| 192 | +***403 Tier Restricted*** |
| 193 | +```json |
| 194 | +{ |
| 195 | + "error": { |
| 196 | + "code": "tier_restricted", |
| 197 | + "message": "The 'closing_lines' feature requires Sharp or higher.", |
| 198 | + "required_tier": "sharp", |
| 199 | + "docs": "https://docs.sharpapi.io/en/pricing" |
| 200 | + } |
| 201 | +} |
| 202 | +``` |
| 203 | + |
| 204 | +***400 Validation Error*** |
| 205 | +```json |
| 206 | +{ |
| 207 | + "error": { |
| 208 | + "code": "validation_error", |
| 209 | + "message": "Invalid group_by. Must be one of: day, league, market, sport, sportsbook" |
| 210 | + } |
| 211 | +} |
| 212 | +``` |
| 213 | + |
| 214 | +## Response Fields |
| 215 | + |
| 216 | +### `data` object |
| 217 | + |
| 218 | +| Field | Type | Description | |
| 219 | +|-------|------|-------------| |
| 220 | +| `group_by` | string | The dimension used for aggregation | |
| 221 | +| `groups` | array | Array of group rows, sorted by `samples` descending | |
| 222 | +| `date_range` | object | Actual `from`/`to` used after tier clamping | |
| 223 | +| `total_events` | integer | Total events found in the date range (before Pinnacle filter) | |
| 224 | +| `comparable_events` | integer | Events with a Pinnacle closing line available for comparison | |
| 225 | +| `sharp_reference` | string | Sharp book used as the fair-odds reference (`"pinnacle"`) | |
| 226 | + |
| 227 | +### Group row fields |
| 228 | + |
| 229 | +| Field | Type | Description | |
| 230 | +|-------|------|-------------| |
| 231 | +| `group_key` | string | The group identifier (sportsbook name, sport, league, market type, or YYYY-MM-DD date) | |
| 232 | +| `samples` | integer | Number of closing line comparisons in this group | |
| 233 | +| `avg_clv_percent` | number | Average CLV% across all samples. Positive = book closed looser than Pinnacle. | |
| 234 | +| `avg_abs_clv_percent` | number | Average absolute CLV% (direction-agnostic market efficiency measure) | |
| 235 | +| `distinct_books` | integer | Number of distinct books in this group | |
| 236 | +| `distinct_sports` | integer | Number of distinct sports in this group | |
| 237 | + |
| 238 | +### `meta` object |
| 239 | + |
| 240 | +| Field | Type | Description | |
| 241 | +|-------|------|-------------| |
| 242 | +| `source` | string | Always `"valkey:closing_line"` (Phase 1 backend) | |
| 243 | +| `tier_window_days` | integer | Maximum lookback days for your tier (Sharp = 30) | |
| 244 | +| `phase` | string | `"1"` — real-time capture from live transitions. ClickHouse backfill planned for Phase 2. | |
| 245 | +| `updated_at` | string | ISO 8601 response timestamp | |
| 246 | + |
| 247 | +--- |
| 248 | + |
| 249 | +## Interpreting CLV |
| 250 | + |
| 251 | +| CLV% | Interpretation | |
| 252 | +|------|----------------| |
| 253 | +| **> +2%** | Book consistently closed significantly looser than sharp lines — high value captured | |
| 254 | +| **+0.5% to +2%** | Book closed slightly loose — typical for recreational books | |
| 255 | +| **-0.5% to +0.5%** | Near-market close — efficient or mixed | |
| 256 | +| **< -0.5%** | Book tightened into close — may indicate steam chasing or sharp-side action | |
| 257 | + |
| 258 | +A positive average CLV on your bets is the strongest indicator that you're betting into inefficient prices. Over a large sample, positive CLV strongly predicts positive ROI. |
| 259 | + |
| 260 | +## Related Endpoints |
| 261 | + |
| 262 | +- [Closing Odds by Date](/en/api-reference/historical-odds-closing) — Per-event raw closing prices for a given date |
| 263 | +- [+EV Opportunities](/en/api-reference/opportunities-ev) — Real-time positive expected value bets |
| 264 | +- [Odds Snapshot](/en/api-reference/odds) — Current pre-match and live odds |
0 commit comments