Эмулятор биржи. Цены берутся вживую с Binance (BTCUSDT, ETHUSDT, SOLUSDT), сделки исполняются по стакану со спредом, комиссией и слиппеджем. Денег настоящих нет — это «бумажная» торговля для проверки стратегий.
Быстрый старт
Полный цикл: посмотреть цену → открыть счёт → купить → узнать итог.
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
"83187.9300000000"), не числа.
Это сделано намеренно: клиент на Python/JS на числах с плавающей точкой теряет копейки,
и «профит» расходится с балансом. Приводите к Decimal/BigDecimal на своей стороне.
Правила, которые важно знать
| Правило | Что это значит |
|---|---|
Покупка по ask, продажа по bid |
Спред всегда против вас — как на реальной бирже. Прибыль не берётся из воздуха. |
| Комиссия 0.1 % | Тейкерская комиссия Binance; списывается с каждой сделки. |
| Слиппедж по стакану | Крупный ордер идёт по уровням стакана; объёма может не хватить — тогда частичное исполнение. |
| Плеча нет | Купить можно только на имеющиеся деньги, продать — только купленное. |
| Стоп-цена: 30 секунд | Если фид молчит, сделка не исполняется (503), а не проходит по старой цене. |
Idempotency-Key |
Повтор с тем же ключом не удваивает сделку — возвращается тот же результат. Нужен при ретраях. |
| PnL = капитал − все вводы | Пополнение счёта и правка баланса руками не считаются прибылью. |
Цены
Все цены фида одним запросом — предпочтительно, чтобы не дёргать сервис по одной.
[{"symbol":"BTCUSDT","bid":"83184.12000000","ask":"83184.13000000","ts":"2026-10-13T19:49:36Z"}]
Одна цена. Если стакан старше 30 с — 503.
{"symbol":"BTCUSDT","bid":"83184.12000000","ask":"83184.13000000","ts":"2026-10-13T19:49:36Z"}
Счета
| Поле | Тип | |
|---|---|---|
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"}
Все счета с итогом каждого — для сравнения стратегий. Это то, что показывает панель.
Один счёт с итогом.
{"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"}
Удалить счёт вместе со всей историей (ордера, исполнения, позиции, депозиты,
кривая капитала). Ответ 204, повторный GET → 404.
Деньги: фантомный баланс
pnl после них не меняется.
Прямого «присвоить баланс» без журнала нет и не будет — иначе прибыль можно нарисовать.
Пополнить (плюс) или снять (минус).
| Поле | Тип | |
|---|---|---|
amount | строка-число | не ноль; минус = снятие |
reason | строка? | комментарий в журнал |
curl -s -X POST $BASE/accounts/$ACC/deposits -H 'Content-Type: application/json' \
-d '{"amount":"5000","reason":"докидка"}'
Выставить кэш счёта ровно в указанную сумму (дельту сервис посчитает сам и запишет в журнал). Если сумма совпадает — ничего не пишется.
curl -s -X PUT $BASE/accounts/$ACC/cash -H 'Content-Type: application/json' \
-d '{"target":"20000","reason":"ручная правка"}'
Торговля
Купить или продать по рынку. Заголовок 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 |
это нормальный исход, не поломка; пополнить счёт или уменьшить объём |
503 | PRICE_UNAVAILABLE — стакан устарел или цену не получить |
подождать и повторить; сделка по старой цене НЕ пройдёт |
Прочее
| Состояние сервиса | … — отдельный порт, {"status":"UP"} |
| Исходный код | git.binom.pw/invest/paper-exchange |
| Панель наблюдения | таблица счетов с кривой капитала и кнопками управления |
Итог считается как equity − (стартовый баланс + все вводы).
Оценка позиций — по bid (цена реальной продажи), поэтому спред и комиссия всегда в минусе.