ピナクルオッズをPostgresに保存する
回答: 市場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 上のハイパーテーブルにしてください。