On the wire · 04:50:02 UTC Live 550 · Prematch 14,382 · Markets 56,955 Vol. I · No. 5 · Friday 13 November 2026 wss://pinnfeed.io/v1/stream
PinnFeed
Réponse · Postgres · Stockage

Stocker les cotes Pinnacle dans Postgres

Mis à jour le · PinnFeed · 2 min de lecture

Réponse : gardez une table append-only de ticks de prix indexée par market id, issue et horodatage, et écrivez-y chaque upsert market du WebSocket PinnFeed. Les snapshots donnent l'état de départ ; les frames delta donnent chaque changement. Ajoutez plus tard les lignes /v1/closing pour le CLV. Fonctionne avec tout Postgres (self-hosted, Supabase, Neon).

Schéma#

create table if not exists odds_ticks (
  ts          timestamptz not null default now(),
  event_id    bigint      not null,
  market_id   text        not null,
  market      text        not null,   -- moneyline | spread | total | team_total
  period      int,
  line        numeric,
  outcome     text        not null,
  decimal     numeric     not null,
  fair        numeric,
  max_stake   numeric,
  primary key (market_id, outcome, ts)
);
create index if not exists odds_ticks_event on odds_ticks (event_id, ts);

Worker Node (Builder)#

// npm i ws pg   ·   PINNFEED_KEY=ps_live_... DATABASE_URL=postgres://... node worker.js
import WebSocket from 'ws';
import pg from 'pg';

const db = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const sql = `insert into odds_ticks (event_id, market_id, market, period, line, outcome, decimal, fair, max_stake)
             values ($1,$2,$3,$4,$5,$6,$7,$8,$9) on conflict do nothing`;

async function save(m) {
  for (const o of m.outcomes || []) {
    await db.query(sql, [m.eventId, m.id, m.type, m.period, m.line ?? null, o.name, o.decimal, o.fair ?? null, m.maxStake ?? null]);
  }
}

function connect() {
  const ws = new WebSocket('wss://pinnfeed.io/v1/stream', { headers: { 'x-api-key': process.env.PINNFEED_KEY } });
  ws.on('open', () => ws.send(JSON.stringify({ type: 'subscribe', sports: ['soccer'], markets: ['moneyline', 'total'], main: true })));
  ws.on('message', async (raw) => {
    const msg = JSON.parse(raw);
    if (msg.type === 'ping') return ws.send(JSON.stringify({ type: 'pong' }));
    if (msg.type === 'snapshot') for (const m of msg.markets || []) await save(m);
    if (msg.type === 'delta') for (const ch of msg.changes) if (ch.kind === 'market' && ch.op === 'upsert') await save(ch.market);
  });
  ws.on('close', (code) => { console.log('closed', code); setTimeout(connect, 2000); });
}
connect();

Si les inserts prennent du retard sur le flux, le serveur peut fermer avec 4008 slow_consumer ; regroupez les inserts (ou mettez-les en file) et abonnez-vous à moins de marchés. Reconnexion complète : reconnexion Node.

Exemples de requêtes#

-- last price per outcome
select distinct on (market_id, outcome) market_id, outcome, decimal, ts
from odds_ticks order by market_id, outcome, ts desc;

Utilisez la table pour backtester contre les lignes de clôture et graphiques de mouvement de ligne (EN).

Offres#

Le streaming demande Builder (99 €/mois) ou Pro ; l'historique des clôtures est inclus sur Builder. Sur Free ou Hobby vous pouvez poller /v1/odds dans la même table. Plans payants : essai de 3 jours ; carte requise, débitée après le jour 3 sauf résiliation.

Contenu éducatif pour adultes (18+) : ni pronostics ni promesse de gain. En France, seuls les opérateurs agréés par l'ANJ peuvent proposer des paris en ligne.

Questions fréquentes#

Faut-il stocker le snapshot ?#

Oui, comme première ligne par issue. Sinon la table ne contient que les prix changés après la connexion.

Comment éviter les doublons après reconnexion ?#

Le snapshot après reconnexion répète les prix courants ; on conflict do nothing avec une clé d'horodatage les garde comme nouveaux ticks. Si vous ne voulez que les changements, comparez au dernier prix stocké avant d'insérer.

TimescaleDB ?#

Même principe ; faites d'odds_ticks une hypertable sur ts.

Le plan WebSocket : Builder, 99 €/mois.

Les cotes Pinnacle en temps réel sur un seul WebSocket : les 13 sports, tous les marchés, cotes justes sans marge et cotes de clôture. 3 jours gratuits, résiliable en un clic.

Form K-1 · Free API key

Get your free API key.

One field. No password, no card: enter your email, click the link we send, and your key is on screen.

By continuing you agree to the Terms and Privacy Policy.

One free key per email. Link valid for 20 minutes.