On the wire · 04:49:13 UTC Live 550 · Prematch 14,382 · Markets 56,971 Vol. I · No. 5 · Friday 13 November 2026 wss://pinnfeed.io/v1/stream
PinnFeed
回答 · Postgres · ストレージ

ピナクルオッズをPostgresに保存する

更新日 · PinnFeed · 約1分で読めます

回答: 市場ID・アウトカム・タイムスタンプをキーにした追記専用の価格ティック表を用意し、PinnFeed WebSocketの各 market upsertを書き込みます。スナップショットが初期状態、delta フレームが各変更です。後から /v1/closing 行を足してCLVに使えます。任意のPostgres(自前、Supabase、Neon)で動作します。

スキーマ#

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);

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();

insertがストリームに遅れると、サーバーは 4008 slow_consumer で閉じることがあります。insertをバッチ(またはキュー)にし、購読する市場を減らしてください。再接続の全体像:Node再接続。

クエリ例#

-- 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;

テーブルは終値に対するバックテストやライン移動チャート(英語)に使えます。

プラン#

ストリーミングにはBuilder(月額99ユーロ)またはProが必要です。終値履歴はBuilderに含まれます。FreeまたはHobbyでは同じテーブルへ /v1/odds をポーリングできます。有料プラン:3日間のトライアル。カード登録が必要で、解約しない場合は3日目の後に課金されます。

18歳以上の方を対象とした教育目的のコンテンツです。予想や利益の約束ではありません。日本では刑法により賭博は原則として禁止されています。

よくある質問#

スナップショットも保存しますか?#

はい。アウトカムごとの最初の行として。さもないと、接続後に変わった価格しか残りません。

再接続後の重複をどう避ける?#

再接続後のスナップショットは現在価格を繰り返します。タイムスタンプキー付きの on conflict do nothing で新しいティックとして残せます。変更だけ欲しければ、insert前に直近保存価格と比較してください。

TimescaleDBは?#

同じです。odds_ticks を ts 上のハイパーテーブルにしてください。

WebSocketプラン:Builder、月額99ユーロ

ピナクルのオッズを1本のWebSocketでリアルタイムに。全13競技、全マーケット、マージンなしのフェアオッズとクロージングオッズ。3日間無料、ワンクリックで解約できます。

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.