Программный доступ к торговле в Upscale с личным API-ключом. Только перечисленные здесь эндпоинты принимают API-ключ — всё остальное требует интерактивной сессии.
Быстрый старт
Открытие и закрытие позиции занимает пять вызовов. Всё ниже выполняется на https://api.upscale.trade с одним
личным API-ключом в заголовке Authorization — см. Аутентификация, чтобы узнать, как его получить.
| # | Вызов | Что это даёт вам |
|---|---|---|
| 1 | GET /accounts/with-risk-status | Счета, которыми ключ может торговать, каждый со своей фазой, статусом, балансом и эквити. Возьмите accountId у того, на котором будете торговать. |
| 2 | GET /v2/markets?accountId=… | Торгуемые рынки с ценой, комиссиями и границами кредитного плеча, по которым проверяется ордер на этом счёте. Возьмите id рынка. |
| 3 | POST /orders с type: "market" | Открывает позицию. amount — это резерв в котируемой валюте, который нужно потратить, leverage — множитель. |
| 4 | GET /positions/{accountId}/active | Открытые позиции. Нужно, потому что ответ ордера не содержит идентификатор позиции. |
| 5 | POST /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 с кодом, указывающим проблемную комбинацию полей; каждая торговая
операция перечисляет те, которые она может выдать.
Операции
-
GET /accounts/{accountId}/equity-history — Получить историю эквити счета трейдера
-
GET /accounts/{accountId}/risk-status — Получить статус риска торгового аккаунта
-
GET /accounts/{accountId}/stats — Получить торговую статистику аккаунта трейдера
-
POST /accounts/{accountId}/close-all — Закрыть все позиции и ордера
-
GET /accounts/risk-status — Получить статус риска всех аккаунтов текущего пользователя
-
GET /accounts/with-risk-status — Получить все счета текущего пользователя со статусом риска
-
GET /orders/{accountId}/active — Получить все активные ордера
-
GET /orders/{accountId}/{asset}/active — Получить активные ордера по тикеру
-
GET /orders/{accountId}/{asset}/history — Получить историю ордеров по тикеру
-
GET /positions/{accountId}/active — Получить все активные позиции
-
GET /positions/{accountId}/portfolio/history — Получить всю историю позиций
-
GET /positions/{positionId}/history — Получить все события по позиции
-
PATCH /positions/{positionId}/margin — Изменить маржу позиции
-
GET /positions/{accountId}/{asset}/history — Получить историю позиций по тикеру
-
GET /positions/{accountId}/{asset}/active — Получить активные позиции по тикеру
-
GET /positions/{accountId}/{asset}/open-notional — Получить текущий открытый интерес по тикеру
-
GET /positions/{accountId}/{asset}/scalping-coefficient — Получить коэффициент скальпинга для тикера