Руководства
Бот для маркет-мейкинга
Готовый бот для маркет-мейкинга на Seeryx на TypeScript и Python: котировки вокруг середины спреда, их обновление, исполнения и награды за ликвидность.
Цикл
Маркет-мейкер держит в стакане бид и аск и зарабатывает на спреде и возврате комиссии мейкеру, когда другие торгуют с ним. На Seeryx достаточно двух бидов: бид на «Вниз» по q — это аск на «Вверх» по 100 − q. Бот ниже выставляет котировки в текущем 5-минутном раунде BTC:
- Читает раунд, его стакан «Вверх» и ваш аккаунт (позиции и ордера в стакане).
- Рассчитывает цены обеих котировок на несколько центов по обе стороны от середины между лучшими бидом и аском. Если стакан не двусторонний, середины нет, и бот ждёт.
- Обновляет котировки, только когда середина сдвинулась или котировка исполнилась: отменяет ордера раунда одним запросом и выставляет обе котировки одним пакетом в режиме «Только мейкер».
- Обрабатывает исполнения. Сообщает, что изменилось, перестаёт покупать сторону, когда её накоплено достаточно, и объединяет пары «Вверх» и «Вниз» обратно в средства.
- Останавливается до закрытия. За 15 секунд до конца отменяет всё и ждёт следующего раунда. То, что у него осталось, выплачивается автоматически.
Бот
Запускайте его рядом с seeryx, message и chain с предыдущих страниц: npx tsx bot.ts или python bot.py. Он входит в аккаунт, открывает торговую сессию (одна подпись кошелька) и повторяет цикл каждые 2 секунды.
// 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-минутные криптовалютные рынки), и на всех событийных рынках. Раз в минуту каждый ордер оценивается относительно середины стакана «Вверх» этого рынка:
| Правило | Значение |
|---|---|
| стоимость | Размер ордера в стакане по его собственной цене, в долларах (покупка «Вниз» по 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при запуске: размеры комиссий и минимальный ордер могут меняться.