PolymarketClickHouse Workshops

02 Modelar os dados

Crie tabelas tipadas de mercados, ticks, negociações e agregados de um minuto no ClickHouse Cloud.

Seu computador
Terminal do macOS: Execute os comandos do workshop no Terminal usando zsh ou bash.

Ponto de partida

.env.polymarket está carregado e você sabe por que IDs de condição e IDs de token são diferentes.

Por que estas tabelas

As cinco consultas posteriores leem janelas de tempo recentes em todos os mercados observados. Por isso, as chaves das tabelas de eventos começam com uma chave de hora/tempo, seguida pelo token ou condição usado no agrupamento. Campos conhecidos usam tipos nativos: UInt256 para IDs de token, DateTime64 para o horário do evento, decimais exatos para preços e tamanhos e enums para valores limitados. O payload opaco da origem permanece como string, pois nenhuma consulta lê seus campos.

Não há chave de partição. Este workshop curto não tem um limite de retenção comprovado; adicionar partições antes de existir um requisito de ciclo de vida criaria partes pequenas sem benefício.

Etapa 1 — Criar o banco e as tabelas brutas

Copie o bloco inteiro para o SQL Console do ClickHouse Cloud e execute-o:

CREATE DATABASE IF NOT EXISTS polymarket;

CREATE TABLE IF NOT EXISTS polymarket.markets
(
    market_id UInt64,
    condition_id FixedString(66),
    token_id UInt256,
    outcome LowCardinality(String),
    question String,
    slug String,
    active Bool,
    accepting_orders Bool,
    volume_24h Decimal128(8),
    observed_at DateTime64(3, 'UTC')
)
ENGINE = ReplacingMergeTree(observed_at)
ORDER BY (condition_id, token_id);

CREATE TABLE IF NOT EXISTS polymarket.price_ticks
(
    event_id FixedString(64),
    condition_id FixedString(66),
    token_id UInt256,
    event_at DateTime64(3, 'UTC'),
    observed_at DateTime64(3, 'UTC'),
    event_kind Enum8(
        'book_snapshot' = 1,
        'price_change' = 2,
        'last_trade_price' = 3,
        'best_bid_ask' = 4,
        'rest_book' = 5
    ),
    source Enum8('WEBSOCKET' = 1, 'CLOB_REST' = 2, 'FIXTURE' = 3),
    price Decimal64(12),
    size Decimal128(8),
    side Enum8('UNKNOWN' = 0, 'BUY' = 1, 'SELL' = 2),
    best_bid Decimal64(12),
    best_ask Decimal64(12),
    midpoint Decimal64(12),
    source_hash String,
    raw_payload String
)
ENGINE = MergeTree
ORDER BY (toStartOfHour(event_at), token_id, event_at, event_id);

CREATE TABLE IF NOT EXISTS polymarket.trades
(
    trade_id FixedString(64),
    condition_id FixedString(66),
    token_id UInt256,
    event_at DateTime64(3, 'UTC'),
    observed_at DateTime64(3, 'UTC'),
    proxy_wallet FixedString(42),
    side Enum8('UNKNOWN' = 0, 'BUY' = 1, 'SELL' = 2),
    price Decimal64(12),
    size Decimal128(8),
    outcome LowCardinality(String),
    transaction_hash FixedString(66),
    title String
)
ENGINE = ReplacingMergeTree(observed_at)
ORDER BY (toStartOfHour(event_at), condition_id, event_at, trade_id);

CREATE OR REPLACE VIEW polymarket.trades_clean AS
SELECT *
FROM polymarket.trades FINAL;

O coletor evita duplicatas antes da inserção. ReplacingMergeTree é uma segunda proteção. A view trades_clean torna determinísticas as pequenas consultas do workshop enquanto os merges ainda estão em andamento.

Etapa 2 — Criar o agregado de midpoint por minuto

CREATE TABLE IF NOT EXISTS polymarket.market_midpoints_1m
(
    token_id UInt256,
    minute DateTime('UTC'),
    open AggregateFunction(argMin, Decimal64(12), Tuple(DateTime64(3, 'UTC'), FixedString(64))),
    high AggregateFunction(max, Decimal64(12)),
    low AggregateFunction(min, Decimal64(12)),
    close AggregateFunction(argMax, Decimal64(12), Tuple(DateTime64(3, 'UTC'), FixedString(64))),
    updates AggregateFunction(count)
)
ENGINE = AggregatingMergeTree
ORDER BY (minute, token_id);

CREATE MATERIALIZED VIEW IF NOT EXISTS polymarket.market_midpoints_1m_mv
TO polymarket.market_midpoints_1m
AS
SELECT
    token_id,
    toStartOfMinute(event_at) AS minute,
    argMinState(midpoint, tuple(event_at, event_id)) AS open,
    maxState(midpoint) AS high,
    minState(midpoint) AS low,
    argMaxState(midpoint, tuple(event_at, event_id)) AS close,
    countState() AS updates
FROM polymarket.price_ticks
WHERE midpoint > 0
  AND event_kind IN ('book_snapshot', 'price_change', 'best_bid_ask', 'rest_book')
GROUP BY token_id, minute;

Esta materialized view agrega somente os midpoints das cotações. Ela exclui deliberadamente o preço alterado no nível da ordem e o preço da última negociação, para que a série OHLC tenha um único significado.

Etapa 3 — Verificar todos os objetos

clickhouse client \
  --host "$CLICKHOUSE_HOST" \
  --port "$CLICKHOUSE_PORT" \
  --user "$CLICKHOUSE_USER" \
  --password "$CLICKHOUSE_PASSWORD" \
  --secure \
  --query "SHOW TABLES FROM polymarket"

Os nomes esperados incluem:

market_midpoints_1m
market_midpoints_1m_mv
markets
price_ticks
trades
trades_clean

Concluído quando

SHOW TABLES retornar os seis objetos sem nenhum servidor ClickHouse local em execução.

Próximo: iniciar o coletor ao vivo.

Nesta página

Acompanhar seu progresso?

Opcional. Enviaremos um link por e-mail para confirmar seu endereço; o progresso será registrado depois que você o abrir.

Use seu e-mail corporativo, não um endereço pessoal.

O acompanhamento do progresso também exige a aceitação dos Termos de Serviço atuais nas Configurações de privacidade.

PT