inicio

Guías

Bot de creación de mercado

Bot completo de creación de mercado para Seeryx en TypeScript y Python: cotiza en torno al punto medio, recotiza, gestiona ejecuciones y gana recompensas.

El ciclo

Un creador de mercado mantiene una compra y una venta en el libro y gana el spread y el reembolso maker cuando otros operan con él. En Seeryx bastan dos órdenes de compra: una compra de Baja a q es una venta de Sube a 100 − q. El bot de abajo cotiza la ronda en vivo de BTC de 5 minutos:

  1. Lee la ronda, su libro de Sube y tu cuenta (posiciones y órdenes en el libro).
  2. Fija el precio de ambas cotizaciones unos centavos a cada lado del punto medio entre la mejor compra y la mejor venta. Sin un libro con ambos lados no hay punto medio, así que espera.
  3. Recotiza solo cuando el punto medio se movió o se ejecutó una cotización: cancela las órdenes de la ronda en una solicitud y coloca ambas cotizaciones en un lote, solo maker.
  4. Gestiona las ejecuciones. Informa de lo que cambió, deja de comprar un lado cuando ya tiene suficiente de él y combina los pares de Sube y Baja de nuevo en efectivo.
  5. Se detiene antes del cierre. 15 segundos antes del final lo cancela todo y espera a la siguiente ronda. Lo que aún tenga se paga por sí solo.

El bot

Ejecútalo junto a seeryx, message y chain de las páginas anteriores: npx tsx bot.ts o python bot.py. Inicia sesión, abre una sesión de trading (una firma de la billetera) y repite el ciclo cada 2 segundos.

bot.ts
// bot.ts: quotes both sides of the live BTC 5-minute round with post-only limit orders. npx tsx bot.ts
import { api, loadWallet, signIn, type Account, type Book, type Round } from './seeryx';
import { signOrder, signPair, tradingSession, type Funds, type Order } from './chain';
import { logValue } from './api-url.mjs';

const MARKET = '/markets/BTC/5m/current';
const SIZE = 10;       // shares per quote (at least minOrderShares of GET /api/fees)
const EDGE = 2;        // cents on each side of the midpoint
const MAX_HELD = 100;  // stop bidding a side once this many of its shares are held
const STOP_S = 15;     // leave the round this many seconds before its close (new orders stop 5 s before)
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
type Results = { results: { status: number; body: { order?: number; error?: string } }[] };

const wallet = await loadWallet();
await signIn(wallet);
const funds = await api<Funds>('/funds');
const { minOrderShares } = await api<{ minOrderShares: number }>('/fees');
// every order is signed by a trading session (one wallet signature now, then none)
const session = funds.onChainTrading ? await tradingSession(funds, wallet) : null;
const sign = (order: Order) => (session ? signOrder(funds, session, wallet.address, order) : order);
let quoted = '';
let seen = new Map<string, number>(); // shares held per side, to report fills

for (;;) {
  try {
    const round = await api<Round>(MARKET);
    if (round.end - Date.now() / 1000 < STOP_S) {
      // step aside before the close: what is still held pays out by itself when the round resolves
      await api(`/orders?slug=${round.slug}`, { method: 'DELETE' });
      await sleep((round.end + 2) * 1000 - Date.now());
      quoted = '';
      continue;
    }
    const [book, account] = await Promise.all([
      api<Book>(`/orderbook/${round.slug}?side=up`), // levels are [cents, shares], best first
      api<Account>('/account'),
    ]);
    const held = (side: string) =>
      account.positions.find((p) => p.slug === round.slug && p.side === side)?.shares ?? 0;
    for (const side of ['up', 'down']) {
      const change = held(side) - (seen.get(side) ?? 0);
      if (Math.abs(change) > 1e-9) console.log('filled', side, logValue(change));
    }
    seen = new Map([['up', held('up')], ['down', held('down')]]);

    // an Up and a Down share together always pay $1: merge pairs back into cash to free it for new quotes
    const pairs = Math.floor(Math.min(held('up'), held('down')));
    if (pairs >= minOrderShares) {
      const signed = session ? await signPair(funds, session, wallet.address, round.slug, true, pairs) : undefined;
      await api('/merge', { method: 'POST', body: { slug: round.slug, shares: pairs, signed } });
      continue;
    }

    const bid = book.bids[0]?.[0], ask = book.asks[0]?.[0];
    if (bid === undefined || ask === undefined) { await sleep(3000); continue; } // no two-sided book: no midpoint
    const mid = (bid + ask) / 2;
    // a Down bid at q is an Up ask at 100 - q: two bids quote both sides without holding shares
    const quotes = [
      { side: 'up' as const, limit: Math.floor(mid - EDGE) },
      { side: 'down' as const, limit: Math.floor(100 - mid - EDGE) },
    ].filter((q) => q.limit >= 1 && q.limit <= 99 && held(q.side) < MAX_HELD);
    const resting = account.orders.filter((o) => o.slug === round.slug).length;
    const key = `${round.slug} ${quotes.map((q) => q.side + q.limit).join(' ')}`;
    if (key !== quoted || resting !== quotes.length) { // the midpoint moved or a quote was filled: requote
      await api(`/orders?slug=${round.slug}`, { method: 'DELETE' });
      const base = { slug: round.slug, action: 'buy', type: 'limit', shares: SIZE, postOnly: true } as const;
      const orders: Order[] = quotes.map((q) => ({ ...base, ...q })); // post only: never takes, never pays the fee
      const body = { orders: await Promise.all(orders.map(sign)) };
      const { results } = await api<Results>('/orders/batch', { method: 'POST', body });
      results.forEach((r, i) => console.log(quotes[i].side, quotes[i].limit, logValue(r.body.order ?? r.body.error)));
      quoted = key;
    }
  } catch (error) {
    const e = error as { message: string; status?: number; retryAfter?: number };
    console.error(logValue(e.message));
    if (e.status === 401 || e.status === 403) await signIn(wallet); // the 7-day sign-in token expired
    if (e.retryAfter) await sleep(e.retryAfter * 1000); // 429 or 503: wait as told
  }
  await sleep(2000);
}

Ejecuciones e inventario

  • Una ejecución es una posición. Una compra de Sube ejecutada te deja con acciones de Sube: pagan 1,00 $ cada una si gana Sube y nada si pierde. MAX_HELD limita cuánto acumula el bot de un mismo lado.
  • Los pares son efectivo. Tener ambos lados equivale a 1,00 $ seguro por par, así que el bot combina los pares de nuevo en USDC al instante (gratis) y vuelve a cotizar con ese efectivo.
  • Solo maker mantiene cada cotización como maker: sin comisión taker y con el reembolso del 20 % en cada ejecución. Una cotización que cruzaría el libro se rechaza (would_take) y simplemente se vuelve a colocar en la siguiente pasada.
  • Ejecuciones más rápidas. El bot consulta periódicamente; para reaccionar antes, suscríbete a la ronda y a la cuenta por WebSocket y recotiza con cada mensaje.
  • Hazlo tuyo: fija el precio a partir de una señal externa (el movimiento del precio del activo subyacente desde que se abrió la ronda) en lugar del punto medio del libro, sesga las cotizaciones en contra de lo que tienes y cotiza las próximas rondas y más mercados.

Recompensas por liquidez

Las órdenes límite en el libro cerca del punto medio ganan puntos de recompensas en los mercados donde liquidityRewards de GET /api/markets es true (los mercados cripto de 5 y 15 minutos) y en todos los mercados de eventos. Una vez por minuto, cada orden se puntúa respecto al punto medio del libro de Sube de ese mercado:

puntuación=valor×((3¢ − distancia) / 3¢)²
ReglaValor
valorEl tamaño en el libro de la orden a su propio precio, en dólares (una compra de Baja a p cuenta como una venta de Sube a 100 − p)
distanciaLo lejos que está la orden del punto medio entre la mejor compra y la mejor venta de Sube; a 3¢ o más no gana nada
Puntos0,1 puntos por dólar de puntuación y por hora, abonados minuto a minuto, por día UTC
Ambos lados+50 % en un mercado donde tus órdenes puntúan en la compra y en la venta
CuentanÓrdenes que llevan al menos 60 segundos en el libro, de al menos la orden mínima y 5 $, en una ronda en vivo con un libro con ambos lados
Tope500 $ de valor por mercado y lado, primero las órdenes más cercanas

GET /api/rewards/liquidity devuelve tus puntos, el ritmo al que ganan puntos tus órdenes en el libro ahora mismo y estas reglas en forma de números. Los puntos cuentan para los niveles de Recompensas.

Antes de ejecutarlo

  • Mantén wallet.json y session.json legibles solo por ti; la clave de sesión puede operar con tu efectivo dentro de sus límites.
  • Gestiona el 429 esperando lo que indique Retry-After; el bot ya lo hace.
  • Lee GET /api/fees al empezar: las tasas y la orden mínima pueden cambiar.