головна сторінка

Посібники

Бот для маркетмейкінгу

Повний бот для маркетмейкінгу на 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 під час запуску: розміри комісій і мінімальний ордер можуть змінюватися.