главная

Руководства

Бот для маркет-мейкинга

Готовый бот для маркет-мейкинга на Seeryx на TypeScript и Python: котировки вокруг середины спреда, их обновление, исполнения и награды за ликвидность.

Цикл

Маркет-мейкер держит в стакане бид и аск и зарабатывает на спреде и возврате комиссии мейкеру, когда другие торгуют с ним. На Seeryx достаточно двух бидов: бид на «Вниз» по q — это аск на «Вверх» по 100 − q. Бот ниже выставляет котировки в текущем 5-минутном раунде BTC:

  1. Читает раунд, его стакан «Вверх» и ваш аккаунт (позиции и ордера в стакане).
  2. Рассчитывает цены обеих котировок на несколько центов по обе стороны от середины между лучшими бидом и аском. Если стакан не двусторонний, середины нет, и бот ждёт.
  3. Обновляет котировки, только когда середина сдвинулась или котировка исполнилась: отменяет ордера раунда одним запросом и выставляет обе котировки одним пакетом в режиме «Только мейкер».
  4. Обрабатывает исполнения. Сообщает, что изменилось, перестаёт покупать сторону, когда её накоплено достаточно, и объединяет пары «Вверх» и «Вниз» обратно в средства.
  5. Останавливается до закрытия. За 15 секунд до конца отменяет всё и ждёт следующего раунда. То, что у него осталось, выплачивается автоматически.

Бот

Запускайте его рядом с seeryx, message и chain с предыдущих страниц: npx tsx bot.ts или python bot.py. Он входит в аккаунт, открывает торговую сессию (одна подпись кошелька) и повторяет цикл каждые 2 секунды.

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

Исполнения и позиции

  • Исполнение — это позиция. Исполненный бид на «Вверх» оставляет у вас доли «Вверх»: каждая приносит 1,00 $, если выиграет «Вверх», и ничего, если проиграет. MAX_HELD ограничивает, сколько долей одной стороны накапливает бот.
  • Пары — это деньги. Обе стороны вместе — гарантированные 1,00 $ за пару, поэтому бот сразу объединяет пары обратно в USDC (бесплатно) и снова выставляет котировки на эти средства.
  • «Только мейкер» оставляет каждую котировку мейкерской: без комиссии тейкера и с возвратом 20 % с каждого исполнения. Котировка, которая пересекла бы стакан, отклоняется (would_take) и просто выставляется снова на следующем проходе.
  • Более быстрые исполнения. Бот опрашивает сервер; чтобы реагировать быстрее, подпишитесь на раунд и аккаунт через WebSocket и обновляйте котировки по каждому сообщению.
  • Настройте под себя: рассчитывайте цену по внешнему сигналу (движению цены базового актива с открытия раунда), а не по середине стакана, смещайте котировки против своей позиции, котируйте предстоящие раунды и другие рынки.

Награды за ликвидность

Лимитные ордера в стакане около середины спреда приносят баллы наград на рынках, где liquidityRewards в GET /api/markets равно true (5- и 15-минутные криптовалютные рынки), и на всех событийных рынках. Раз в минуту каждый ордер оценивается относительно середины стакана «Вверх» этого рынка:

оценка=стоимость×((3¢ − расстояние) / 3¢)²
ПравилоЗначение
стоимостьРазмер ордера в стакане по его собственной цене, в долларах (покупка «Вниз» по p считается аском на «Вверх» по 100 − p)
расстояниеНасколько ордер удалён от середины между лучшими бидом и аском «Вверх»; при 3¢ и более ничего не начисляется
Баллы0,1 балла за доллар оценки в час, начисляются поминутно, за каждый день по UTC
Обе стороны+50 % на рынке, где ваши ордера получают оценку и на стороне покупки, и на стороне продажи
Что учитываетсяОрдера, простоявшие в стакане не менее 60 секунд, размером не меньше минимального ордера и не меньше 5 $, в текущем раунде с двусторонним стаканом
Предел500 $ стоимости на рынок и сторону, начиная с ближайших ордеров

GET /api/rewards/liquidity возвращает ваши баллы, скорость, с которой ваши ордера в стакане зарабатывают их прямо сейчас, и эти правила в числах. Баллы засчитываются в уровни на странице Награды.

Перед запуском

  • Доступ к wallet.json и session.json должен быть только у вас: ключ сессии может торговать вашими средствами в пределах своих лимитов.
  • Обрабатывайте 429, выжидая Retry-After; бот уже это делает.
  • Читайте GET /api/fees при запуске: размеры комиссий и минимальный ордер могут меняться.