Paper Exchange — API

база: … ← к панели

Эмулятор биржи. Цены берутся вживую с Binance (BTCUSDT, ETHUSDT, SOLUSDT), сделки исполняются по стакану со спредом, комиссией и слиппеджем. Денег настоящих нет — это «бумажная» торговля для проверки стратегий.

Один сервис — много счетов. Счёт открывается под каждую стратегию; счета независимы (свои деньги, позиции, PnL и своя кривая капитала) и их можно сравнивать между собой.

Быстрый старт

Полный цикл: посмотреть цену → открыть счёт → купить → узнать итог.

BASE=__BASE__/api/v1

# 1. живые цены
curl -s $BASE/prices

# 2. открыть счёт на 10 000 USDT
ACC=$(curl -s -X POST $BASE/accounts \
  -H 'Content-Type: application/json' \
  -d '{"name":"моя-стратегия","initialCash":"10000"}' \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')

# 3. купить 0.01 BTC по рынку
curl -s -X POST $BASE/accounts/$ACC/orders \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"symbol":"BTCUSDT","side":"BUY","qty":"0.01"}'

# 4. итог: в плюсе или в минусе
curl -s $BASE/accounts/$ACC/pnl
Все суммы в JSON — строки ("83187.9300000000"), не числа. Это сделано намеренно: клиент на Python/JS на числах с плавающей точкой теряет копейки, и «профит» расходится с балансом. Приводите к Decimal/BigDecimal на своей стороне.

Правила, которые важно знать

ПравилоЧто это значит
Покупка по ask, продажа по bid Спред всегда против вас — как на реальной бирже. Прибыль не берётся из воздуха.
Комиссия 0.1 % Тейкерская комиссия Binance; списывается с каждой сделки.
Слиппедж по стакану Крупный ордер идёт по уровням стакана; объёма может не хватить — тогда частичное исполнение.
Плеча нет Купить можно только на имеющиеся деньги, продать — только купленное.
Стоп-цена: 30 секунд Если фид молчит, сделка не исполняется (503), а не проходит по старой цене.
Idempotency-Key Повтор с тем же ключом не удваивает сделку — возвращается тот же результат. Нужен при ретраях.
PnL = капитал − все вводы Пополнение счёта и правка баланса руками не считаются прибылью.

Цены

GET/api/v1/prices

Все цены фида одним запросом — предпочтительно, чтобы не дёргать сервис по одной.

[{"symbol":"BTCUSDT","bid":"83184.12000000","ask":"83184.13000000","ts":"2026-10-13T19:49:36Z"}]
GET/api/v1/price?symbol=BTCUSDT

Одна цена. Если стакан старше 30 с — 503.

{"symbol":"BTCUSDT","bid":"83184.12000000","ask":"83184.13000000","ts":"2026-10-13T19:49:36Z"}

Счета

POST/api/v1/accounts
ПолеТип
nameстрокаимя счёта, уникально
initialCashстрока-числостартовый баланс, > 0
feeRateстрока-число?необязательно; по умолчанию 0.001
curl -s -X POST $BASE/accounts -H 'Content-Type: application/json' \
  -d '{"name":"strategy-a","initialCash":"10000"}'

→ 201
{"id":"b9002a44-…","name":"strategy-a","baseCurrency":"USDT",
 "initialCash":"10000.0000000000","cash":"10000.0000000000",
 "feeRate":"0.001","createdAt":"2026-10-13T19:51:02Z"}
GET/api/v1/accounts

Все счета с итогом каждого — для сравнения стратегий. Это то, что показывает панель.

GET/api/v1/accounts/{id}

Один счёт с итогом.

{"id":"b9002a44-…","name":"strategy-a","cash":"6301.5200000000",
 "positionsValue":"13684.2900000000","equity":"19985.8100000000",
 "netDeposits":"20000.0000000000","pnl":"-14.1857930000","pnlPct":"-0.0007092897",
 "realizedPnl":"0E-10","unrealizedPnl":"-0.5010000000"}
DELETE/api/v1/accounts/{id}

Удалить счёт вместе со всей историей (ордера, исполнения, позиции, депозиты, кривая капитала). Ответ 204, повторный GET → 404.

Деньги: фантомный баланс

Изменение денег — это внешний ввод, а не прибыль. Оба метода ниже пишут запись в журнал вводов, поэтому pnl после них не меняется. Прямого «присвоить баланс» без журнала нет и не будет — иначе прибыль можно нарисовать.
POST/api/v1/accounts/{id}/deposits

Пополнить (плюс) или снять (минус).

ПолеТип
amountстрока-числоне ноль; минус = снятие
reasonстрока?комментарий в журнал
curl -s -X POST $BASE/accounts/$ACC/deposits -H 'Content-Type: application/json' \
  -d '{"amount":"5000","reason":"докидка"}'
PUT/api/v1/accounts/{id}/cash

Выставить кэш счёта ровно в указанную сумму (дельту сервис посчитает сам и запишет в журнал). Если сумма совпадает — ничего не пишется.

curl -s -X PUT $BASE/accounts/$ACC/cash -H 'Content-Type: application/json' \
  -d '{"target":"20000","reason":"ручная правка"}'

Торговля

POST/api/v1/accounts/{id}/orders

Купить или продать по рынку. Заголовок Idempotency-Key необязателен, но настоятельно рекомендуется при ретраях.

ПолеТип
symbolстрокаBTCUSDT / ETHUSDT / SOLUSDT
sideстрокаBUY или SELL
qtyстрока-числоколичество базового актива, > 0
curl -s -X POST $BASE/accounts/$ACC/orders \
  -H 'Content-Type: application/json' -H "Idempotency-Key: k-1" \
  -d '{"symbol":"BTCUSDT","side":"BUY","qty":"0.05"}'

→ 201
{"orderId":"6395f087-…","status":"FILLED","symbol":"BTCUSDT","side":"BUY",
 "qty":"0.0500000000","price":"83178.2600000000","gross":"4158.9130000000",
 "fee":"4.1589130000","cash":"836.9280870000","positionQty":"0.0500000000"}

Если денег или позиции не хватает — 409 с кодом INSUFFICIENT_FUNDS / INSUFFICIENT_POSITION.

Чтение истории и итогов

МетодПутьЧто отдаёт
GET/api/v1/accounts/{id}/pnl итог: initialCash, netDeposits, equity, pnl, pnlPct, realizedPnl, unrealizedPnl
GET/api/v1/accounts/{id}/positions symbol, qty, avgCost, realizedPnl
GET/api/v1/accounts/{id}/orders?limit=100 ордера с вложенными исполнениями; limit по умолчанию 100
GET/api/v1/accounts/{id}/equity?since=…&snapshot=… кривая капитала: ts, cash, positionsValue, equity, realizedPnl, unrealizedPnl. since — ISO-время, snapshot=true — записать точку прямо сейчас

Ошибки

Тело ошибки всегда одно: {"error":"КОД","message":"пояснение"}.

КодКогдаЧто делать
400кривой запрос: нет поля, не число, отрицательное количествоисправить запрос
404счёта нетпроверить id
409бизнес-отказ: INSUFFICIENT_FUNDS / INSUFFICIENT_POSITION это нормальный исход, не поломка; пополнить счёт или уменьшить объём
503PRICE_UNAVAILABLE — стакан устарел или цену не получить подождать и повторить; сделка по старой цене НЕ пройдёт

Прочее

Состояние сервиса… — отдельный порт, {"status":"UP"}
Исходный кодgit.binom.pw/invest/paper-exchange
Панель наблюдениятаблица счетов с кривой капитала и кнопками управления

Итог считается как equity − (стартовый баланс + все вводы). Оценка позиций — по bid (цена реальной продажи), поэтому спред и комиссия всегда в минусе.