Upscale
Menu
On this page

Публичный API Upscale

Версия d796ac9Обновлено

Программный доступ к торговле в Upscale с личным API-ключом. Только перечисленные здесь эндпоинты принимают API-ключ — всё остальное требует интерактивной сессии.

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

Открытие и закрытие позиции занимает пять вызовов. Всё ниже выполняется на https://api.upscale.trade с одним личным API-ключом в заголовке Authorization — см. Аутентификация, чтобы узнать, как его получить.

#ВызовЧто это даёт вам
1GET /accounts/with-risk-statusСчета, которыми ключ может торговать, каждый со своей фазой, статусом, балансом и эквити. Возьмите accountId у того, на котором будете торговать.
2GET /v2/markets?accountId=…Торгуемые рынки с ценой, комиссиями и границами кредитного плеча, по которым проверяется ордер на этом счёте. Возьмите id рынка.
3POST /orders с type: "market"Открывает позицию. amount — это резерв в котируемой валюте, который нужно потратить, leverage — множитель.
4GET /positions/{accountId}/activeОткрытые позиции. Нужно, потому что ответ ордера не содержит идентификатор позиции.
5POST /orders с type: "take" и triggerPrice: "0"Закрывает эту позицию по рынку. amount — это базовый размер для закрытия.

Три вещи, которые нужно определить перед написанием любого кода:

  • Каждое число — это необработанная строка fp9 — значение, умноженное на 10⁹. 100 USD — это "100000000000", 10x плечо — это "10000000000". Читайте и записывайте их с помощью BigInt / Decimal; число с плавающей запятой незаметно отбрасывает последние цифры.
  • amount меняет единицу измерения в зависимости от класса ордера. Для ордера market, который открывает позицию, это quote-сумма — маржа, комиссия, спред и буфер, зарезервированные из свободного баланса. Для ордера take, который её закрывает, это размер позиции в базовом активе, и ничего не резервируется. Шаг 5 передаёт size позиции напрямую.
  • Закрытие — это ордер take, а не рыночный ордер в обратную сторону. Позиции ведутся по направлению, поэтому рыночный ордер short, размещённый против открытой long, открывает вторую позицию вместо закрытия первой. Ордер take с triggerPrice: "0" не имеет триггера и исполняется по рынку, пока вызов ещё открыт; amount, превышающий удерживаемую позицию, закрывает её полностью. Чтобы закрыть все позиции на счёте одним вызовом, используйте POST /positions/{accountId}/close-all.

Оба скрипта ниже полны: задайте UPSCALE_API_KEY в окружении и запустите.

JavaScript (Node 18+)

Сохраните как upscale.mjs и запустите с помощью node upscale.mjs — без зависимостей.

import { randomUUID } from 'node:crypto';
 
const BASE_URL = 'https://api.upscale.trade';
const API_KEY = process.env.UPSCALE_API_KEY;
 
// Human decimal -> fp9 raw string: 100 -> "100000000000", "0.5" -> "500000000".
const toFp9 = (value) => {
  const [whole, fraction = ''] = String(value).split('.');
  return BigInt(whole + fraction.padEnd(9, '0').slice(0, 9)).toString();
};
 
// fp9 raw string -> human decimal string. Stays on BigInt: Number loses the low digits above ~9M USD.
const fromFp9 = (raw) => {
  const negative = raw.startsWith('-');
  const digits = (negative ? raw.slice(1) : raw).padStart(10, '0');
  return `${negative ? '-' : ''}${digits.slice(0, -9)}.${digits.slice(-9)}`;
};
 
const call = async (method, path, body) => {
  const response = await fetch(`${BASE_URL}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      // One key per logical request; a retry of that request must send the same key again.
      ...(body ? { 'Content-Type': 'application/json', 'x-idempotency-key': randomUUID() } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const payload = response.status === 204 ? null : await response.json();
  if (!response.ok) {
    // `error` is the stable machine-readable code — branch on it, never on `message`.
    throw new Error(`${method} ${path} -> ${response.status} ${payload?.error}: ${payload?.message}`);
  }
  return payload;
};
 
// 1. Accounts. Authenticated with an API key the list holds only accounts that have api_trading enabled.
const accounts = await call('GET', '/accounts/with-risk-status');
const account = accounts.find((item) => item.status === 'active' && item.apiTrading && item.type === 'demo');
if (!account) {
  throw new Error('No active demo account with API trading enabled');
}
console.log(`account ${account.accountId}, equity ${fromFp9(account.riskStatus.currentEquity)}`);
 
// 2. Markets, priced by the shard that hosts this account — the state the order will be filled against.
const markets = await call('GET', `/v2/markets?accountId=${account.accountId}`);
const market = markets.find((item) => item.config.baseAsset === 'BTC');
if (!market) {
  throw new Error('BTC is not available on this account');
}
console.log(`${market.config.ticker} at ${fromFp9(market.state.indexPrice)}`);
 
// 3. Open a long: 100 USD of reserve at 10x, filled at market.
const order = await call('POST', '/orders', {
  accountId: account.accountId,
  marketId: market.id,
  type: 'market',
  direction: 'long',
  amount: toFp9(100),
  leverage: toFp9(10),
  expectedAmount: '0', // no slippage check; pass the base size you expect to enforce one
});
console.log(`order ${order.id} ${order.status}`);
 
// 4. Read the position back — the order response does not carry its identifier.
const positions = await call('GET', `/positions/${account.accountId}/active`);
const position = positions.find((item) => item.market === market.id && item.direction === 'long');
if (!position) {
  throw new Error('Position not found — the order may have been deferred or rejected at execution');
}
console.log(`position ${position.idx}, size ${fromFp9(position.size)} ${market.config.baseAsset}`);
 
// 5. Close it at market.
await call('POST', '/orders', {
  accountId: account.accountId,
  marketId: market.id,
  type: 'take',
  direction: position.direction, // a close order runs in the same direction as its position
  positionId: position.idx,
  amount: position.size, // base units
  triggerPrice: '0', // no trigger: fill now
});
console.log('closed');

Python (3.10+)

Требуется requests (pip install requests).

import os
import uuid
from decimal import Decimal
 
import requests
 
BASE_URL = 'https://api.upscale.trade'
API_KEY = os.environ['UPSCALE_API_KEY']
FP9 = Decimal(10) ** 9
 
 
def to_fp9(value) -> str:
    """Human decimal -> fp9 raw string: 100 -> "100000000000"."""
    return str(int(Decimal(str(value)) * FP9))
 
 
def from_fp9(raw: str) -> Decimal:
    """fp9 raw string -> Decimal. Never float — it drops the low digits."""
    return Decimal(raw) / FP9
 
 
def call(method: str, path: str, body: dict | None = None):
    headers = {'Authorization': f'Bearer {API_KEY}'}
    if body is not None:
        # One key per logical request; a retry of that request must send the same key again.
        headers['x-idempotency-key'] = str(uuid.uuid4())
    response = requests.request(method, f'{BASE_URL}{path}', json=body, headers=headers, timeout=30)
    payload = None if response.status_code == 204 else response.json()
    if not response.ok:
        # `error` is the stable machine-readable code — branch on it, never on `message`.
        raise RuntimeError(
            f'{method} {path} -> {response.status_code} {payload.get("error")}: {payload.get("message")}'
        )
    return payload
 
 
# 1. Accounts. Authenticated with an API key the list holds only accounts that have api_trading enabled.
accounts = call('GET', '/accounts/with-risk-status')
account = next((item for item in accounts if item['status'] == 'active' and item['apiTrading'] and item['type'] == 'demo'), None)
if account is None:
    raise SystemExit('No active demo account with API trading enabled')
print(f"account {account['accountId']}, equity {from_fp9(account['riskStatus']['currentEquity'])}")
 
# 2. Markets, priced by the shard that hosts this account — the state the order will be filled against.
markets = call('GET', f"/v2/markets?accountId={account['accountId']}")
market = next((item for item in markets if item['config']['baseAsset'] == 'BTC'), None)
if market is None:
    raise SystemExit('BTC is not available on this account')
print(f"{market['config']['ticker']} at {from_fp9(market['state']['indexPrice'])}")
 
# 3. Open a long: 100 USD of reserve at 10x, filled at market.
order = call('POST', '/orders', {
    'accountId': account['accountId'],
    'marketId': market['id'],
    'type': 'market',
    'direction': 'long',
    'amount': to_fp9(100),
    'leverage': to_fp9(10),
    'expectedAmount': '0',  # no slippage check; pass the base size you expect to enforce one
})
print(f"order {order['id']} {order['status']}")
 
# 4. Read the position back — the order response does not carry its identifier.
positions = call('GET', f"/positions/{account['accountId']}/active")
position = next((item for item in positions if item['market'] == market['id'] and item['direction'] == 'long'), None)
if position is None:
    raise SystemExit('Position not found — the order may have been deferred or rejected at execution')
print(f"position {position['idx']}, size {from_fp9(position['size'])} {market['config']['baseAsset']}")
 
# 5. Close it at market.
call('POST', '/orders', {
    'accountId': account['accountId'],
    'marketId': market['id'],
    'type': 'take',
    'direction': position['direction'],  # a close order runs in the same direction as its position
    'positionId': position['idx'],
    'amount': position['size'],  # base units
    'triggerPrice': '0',  # no trigger: fill now
})
print('closed')

Куда двигаться дальше

  • Отложенные входы (limit, stop_market, stop_limit) и защитные ордера (stop, trailing_stop) используют один и тот же POST /orders маршрут — операция перечисляет, что требуется для каждого типа.
  • Стоп-лосс и тейк-профит могут идти вместе с ордером на открытие как stopTriggerPrice и takeTriggerPrice.
  • GET /accounts/{accountId}/risk-status — это эндпоинт, который нужно опрашивать для проверки запаса по просадке, пока позиция открыта.

Аутентификация

Создайте ключ в приложении Upscale (или с помощью POST /user/api-keys из аутентифицированной сессии) — необработанный ключ показывается один раз, в момент создания. Передавайте его как bearer-токен:

Authorization: Bearer usk_<your key>

Ключ наследует разрешения пользователя, которому он принадлежит, и остаётся действительным, пока он не будет удалён, обновлён или не истечёт.

Торговля через API включается отдельно для каждого аккаунта: аккаунт с выключенным api_trading отвечает 403 (api_trading_not_enabled) на каждый запрос, сделанный с ключом, а списки аккаунтов полностью его исключают.

Соглашения

  • Суммы, цены, кредитное плечо и множители передаются как fp9 необработанные целочисленные строки — значение, масштабированное на 10⁹. "1000000000" — это 1 USD, "10000000000" — это 10x кредитное плечо, "50000000" — это 5%.
  • fp9 фиксирует масштаб, а не единицу измерения. Сумма в котируемой валюте — это USD; размер базового актива — это единицы торгуемого актива. amount в ордере — это котируемая сумма в ордерах на увеличение и базовый размер в ордерах на закрытие — каждое поле указывает, какое из двух значений оно содержит.
  • Идентификаторы — это uuid v4. К рынкам обращаются по идентификатору при торговле и по тикеру базового актива (BTC) на эндпоинтах чтения по каждому рынку.
  • Списки разбиваются на страницы с помощью limit и offset и возвращают общее количество рядом со страницей.
  • Позиции и ордера привязаны к фазе, в которой аккаунт находится прямо сейчас: то, что относится к завершённой фазе, больше не возвращается.

Лимиты запросов

Лимиты считаются по ключу, в фиксированных окнах, отдельно для эндпоинтов чтения (GET) и записи (всё остальное). По умолчанию: 10 запросов в секунду и 60 в минуту для чтения, 10 в секунду и 60 в минуту для записи.

Превышение лимита возвращает 429 (api_key_rate_limit_exceeded) с заголовком Retry-After и телом содержащим бакет (read / write), окно, которое сработало, его лимит и retryAt.

Ключ подчиняется только своим собственным лимитам. Лимиты для отдельных эндпоинтов, с которыми сталкивается приложение Upscale, считаются по каждой интерактивной сессии и не применяются к запросам с API-ключом, поэтому ни один эндпоинт здесь не имеет более строгого собственного лимита.

Ошибки

При сбое возвращается { statusCode, message, error }, где error — стабильный машиночитаемый код. Ветвление выполняйте по этому коду, а не по тексту сообщения.

  • 400 — тело, query или path не прошли проверку схемы (validation_failed), или торговое предусловие было отклонено (insufficient_balance, position_not_available и коды для отдельных операций ниже).
  • 401 — ключ отсутствует, имеет неверный формат, отозван или истёк (session_expired).
  • 403 — ключ был использован на эндпоинте, который не принимает ключи (api_key_not_allowed), счёт принадлежит кому-то другому (account_access_denied), API-торговля отключена (api_trading_not_enabled), счёт больше не торгует (challenge_closed), не достиг фазы, допускающей торговлю (account_phase_not_allowed), или заблокирован лимитом управляемого капитала (funded_limit_trading_locked), ордер больше не активен (order_not_active), или рынок приостановлен (market_paused), работает только на закрытие (market_close_only) или находится вне категории, в которой счёт может торговать (market_category_not_allowed).
  • 404 — нет такого аккаунта (account_not_found), ордера (order_not_found), позиции (position_not_found) или рынка (market_not_found).
  • 409 — запрос с тем же x-idempotency-key всё ещё обрабатывается (idempotency_key_in_flight), аккаунт в данный момент не загружен торговым движком (account_not_loaded), или рыночная цена не обновлялась достаточно недавно, чтобы торговать по ней (market_price_stale).
  • 429 — сработал лимит частоты запросов для ключа (api_key_rate_limit_exceeded).

Отклонения на уровне ордера возвращаются как 400 с кодом, указывающим проблемную комбинацию полей; каждая торговая операция перечисляет те, которые она может выдать.

Операции

Last updated