Skip to content

Commit 5c41564

Browse files
Mlaz-coderootPaperclip-Paperclip
authored
docs(historical): add CLV and closing odds endpoints (Sharp tier) (#201)
Add full reference docs for /api/v1/historical/clv and /api/v1/historical/odds/closing — both now live and Sharp-tier gated (closing_lines feature). Documents real Valkey-backed response schema, 30-day Phase 1 retention window, Power devig math, and tier-restricted 403 errors. Updates pricing.mdx to surface CLV/closing_lines as Sharp features and adds both endpoints to the feature comparison table. Co-authored-by: root <root@api-dev.local> Co-authored-by: Paperclip <noreply@paperclip.ing>
1 parent e0bcf53 commit 5c41564

4 files changed

Lines changed: 588 additions & 1 deletion

File tree

content/en/api-reference/_meta.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ export default {
6969
account: "Account Info",
7070
"account-usage": "Usage Stats",
7171
"account-keys": "API Key Management",
72+
"---Historical": {
73+
type: "separator",
74+
title: "Historical (Sharp+)",
75+
},
76+
"historical-clv": "Closing Line Value (CLV)",
77+
"historical-odds-closing": "Closing Odds by Date",
7278
"---Enterprise": {
7379
type: "separator",
7480
title: "Enterprise",
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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

Comments
 (0)