Quote Pinnacle in tempo reale via WebSocket con Python e Node
In questa guida costruisci un client che tiene in memoria una copia live dei prezzi di Pinnacle: si connette, si iscrive, carica lo snapshot, applica ogni delta, risponde agli heartbeat e si riconnette da solo. Le altre guide (alert di steam, value betting, il bot Telegram) si basano su questo.
Ti serve una chiave PinnFeed sul piano Builder (il piano WebSocket, 99 €/mese, 3 giorni gratis). Con una chiave Free o Hobby puoi seguire gli esempi REST della documentazione (in inglese).
Passo 1 — Ottieni una chiave e mettila nell'ambiente#
Crea una chiave nella dashboard ed esportala, così non finisce mai nel tuo codice:
export PINNFEED_KEY="pf_live_..."
Passo 2 — Capire i frame#
Dopo la connessione e l'invio di subscribe, il server manda:
subscribede poi uno snapshot in parti (part,parts,final) con tutti gli eventi e i mercati che corrispondono ai tuoi filtri;- frame delta, ognuno con una lista
changes:upsertcontiene l'oggetto nuovo completo,removesolo il suo id; - un ping ogni 20 secondi. Rispondi con
{"type":"pong"}o il server chiude il socket dopo 60 secondi di silenzio.
I mercati sono identificati da id (eventId:key), quindi un dizionario è tutto lo stato che ti serve. Ogni esito ha decimal, american, fair (quota equa senza margine) e fairProb. Il riferimento del WebSocket (in inglese) elenca tutti i campi.
Passo 3 — Il client Python#
Installa l'unica dipendenza con pip install websockets (Python 3.9+). Salvalo come pinnfeed_stream.py; le guide sullo steam e su Telegram lo importano.
# pinnfeed_stream.py: mantiene una copia live del book di Pinnacle dal WebSocket di PinnFeed.
# pip install websockets (Python 3.9+)
import asyncio, json, os, random
import websockets
WS_URL = os.environ.get("PINNFEED_WS", "wss://pinnfeed.io/v1/stream")
KEY = os.environ["PINNFEED_KEY"]
SIDES = {"home", "away", "draw", "over", "under"}
class Book:
"""Eventi e mercati per id, aggiornati dai frame snapshot + delta."""
def __init__(self):
self.events, self.markets = {}, {}
def apply(self, msg, on_market=None):
if msg["type"] == "snapshot":
for e in msg["events"]:
self.events[e["id"]] = e
for m in msg["markets"]:
self.markets[m["id"]] = m
elif msg["type"] == "delta":
for c in msg["changes"]:
if c["kind"] == "event" and c["op"] == "upsert":
self.events[c["event"]["id"]] = c["event"]
elif c["kind"] == "event" and c["op"] == "remove":
self.events.pop(c["id"], None)
for mid in [k for k, m in self.markets.items() if m["eventId"] == c["id"]]:
del self.markets[mid]
elif c["kind"] == "market" and c["op"] == "upsert":
old = self.markets.get(c["market"]["id"])
self.markets[c["market"]["id"]] = c["market"]
if on_market:
on_market(old, c["market"], self.events.get(c["market"]["eventId"]))
elif c["kind"] == "market" and c["op"] == "remove":
self.markets.pop(c["id"], None)
async def stream(subscription, on_market=None, on_ready=None):
"""Si connette, si iscrive e tiene aggiornato il book; si riconnette con backoff all'infinito."""
delay = 1
while True:
book = Book()
try:
async with websockets.connect(WS_URL, additional_headers={"x-api-key": KEY}, max_size=None) as ws:
await ws.send(json.dumps({"type": "subscribe", **subscription}))
async for raw in ws:
msg = json.loads(raw)
if msg["type"] == "ping":
await ws.send('{"type":"pong"}')
elif msg["type"] == "error":
print("server error:", msg["code"], msg["message"])
elif msg["type"] in ("snapshot", "delta"):
book.apply(msg, on_market if msg["type"] == "delta" else None)
if msg["type"] == "snapshot" and msg.get("final") and on_ready:
on_ready(book)
delay = 1
except (OSError, websockets.ConnectionClosed) as e:
print(f"disconnected ({e}); reconnecting in {delay}s")
await asyncio.sleep(delay + random.random())
delay = min(delay * 2, 30)
if __name__ == "__main__":
def ready(book):
print(f"book ready: {len(book.events)} events, {len(book.markets)} markets")
def moved(old, new, event):
if old and event:
for o_old, o_new in zip(old["outcomes"], new["outcomes"]):
if o_new["name"] in SIDES and o_old["decimal"] != o_new["decimal"]:
print(f'{event["home"]} v {event["away"]} {new["type"]} {o_new["name"]}: '
f'{o_old["decimal"]} -> {o_new["decimal"]} (fair {o_new["fair"]})')
asyncio.run(stream({"sports": ["soccer"], "markets": ["moneyline"], "main": True, "specials": False}, moved, ready))
Eseguilo con python pinnfeed_stream.py. Vedrai book ready: N events, M markets e poi una riga per ogni movimento di prezzo.
Passo 4 — Il client Node#
La stessa logica in Node 18+ con npm install ws. Salvalo come stream.mjs ed esegui node stream.mjs.
// stream.mjs: book di Pinnacle live dal WebSocket di PinnFeed (Node 18+).
// npm install ws
import WebSocket from 'ws';
const WS_URL = process.env.PINNFEED_WS || 'wss://pinnfeed.io/v1/stream';
const KEY = process.env.PINNFEED_KEY;
const SIDES = new Set(['home', 'away', 'draw', 'over', 'under']);
const events = new Map();
const markets = new Map();
function connect(delay = 1000) {
const ws = new WebSocket(WS_URL, { headers: { 'x-api-key': KEY }, perMessageDeflate: true });
ws.on('open', () => {
delay = 1000;
ws.send(JSON.stringify({ type: 'subscribe', sports: ['soccer', 'tennis'], markets: ['moneyline'], main: true, specials: false }));
});
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
if (msg.type === 'ping') return ws.send('{"type":"pong"}');
if (msg.type === 'error') return console.error('server error:', msg.code, msg.message);
if (msg.type === 'snapshot') {
if (msg.part === 1) { events.clear(); markets.clear(); }
msg.events.forEach((e) => events.set(e.id, e));
msg.markets.forEach((m) => markets.set(m.id, m));
if (msg.final) console.log(`book ready: ${events.size} events, ${markets.size} markets`);
}
if (msg.type === 'delta') {
for (const c of msg.changes) {
if (c.kind === 'event') c.op === 'upsert' ? events.set(c.event.id, c.event) : events.delete(c.id);
if (c.kind === 'market' && c.op === 'remove') markets.delete(c.id);
if (c.kind === 'market' && c.op === 'upsert') {
const old = markets.get(c.market.id);
markets.set(c.market.id, c.market);
const ev = events.get(c.market.eventId);
if (!old || !ev) continue;
c.market.outcomes.forEach((o, i) => {
const prev = old.outcomes[i];
if (prev && SIDES.has(o.name) && prev.decimal !== o.decimal) console.log(`${ev.home} v ${ev.away} ${o.name}: ${prev.decimal} -> ${o.decimal} (fair ${o.fair})`);
});
}
}
}
});
ws.on('close', (code) => {
const wait = Math.min(delay * 2, 30000);
console.log(`closed (${code}); reconnecting in ${wait / 1000}s`);
setTimeout(() => connect(wait), wait + Math.random() * 1000);
});
ws.on('error', (e) => console.error('ws error:', e.message));
}
connect();
Passo 5 — Restringi l'iscrizione#
I filtri si applicano lato server, quindi ricevi ed elabori solo ciò che chiedi:
{ "type": "subscribe", "sports": ["tennis"], "live": true, "markets": ["moneyline", "total"], "main": true }
sports: slug o id;leaguesedevents: liste di id.live:truesolo live,falsesolo pre-match; se lo ometti, entrambi.main: truetiene solo il testa a testa, l'handicap e il totale principali di ogni evento.- Inviare di nuovo
subscribesostituisce l'iscrizione e manda un nuovo snapshot.
Checklist per la produzione#
- Riconnettiti con backoff e jitter (entrambi i client lo fanno: 1 s, 2 s, 4 s, fino a 30 s) e ricostruisci dal nuovo snapshot.
- Attiva la compressione:
websocketsdi Python negoziapermessage-deflatedi default; in Node passaperMessageDeflate: true. Gli snapshot si riducono di 5–10 volte. - Leggi in fretta. Se per un client lento si accumulano più di 16 MB, il server lo chiude con il codice
4008. Fai il lavoro pesante fuori dal ciclo del socket. - Usa
seqper i log, non per rilevare buchi: i filtri saltano i lotti a cui non sei iscritto.
Domande frequenti#
Quale piano mi serve per il WebSocket?#
Builder (99 €/mese) o superiore. Le chiavi Free e Hobby ricevono 403 plan_lacks_ws sul WebSocket e possono usare il REST.
Quante connessioni posso aprire?#
Builder consente 3 connessioni WebSocket per account, Pro 10.
Perché alcuni esiti hanno id di partecipanti invece di home o away?#
Gli speciali e alcuni antepost elencano partecipanti invece di squadra di casa e ospite. Iscriviti con "specials": false e filtra su home, away, draw, over e under se vuoi solo le linee delle partite, come fanno gli esempi.