Upscale
Menu
On this page

API pública de Upscale

Versión d796ac9Último cambio

Acceso programático al trading de Upscale con una clave de API personal. Solo los endpoints listados aquí aceptan una clave de API — todo lo demás requiere una sesión interactiva.

Inicio rápido

Abrir y cerrar una posición requiere cinco llamadas. Todo lo que aparece a continuación se ejecuta contra https://api.upscale.trade con una única clave de API personal en el encabezado Authorization — consulta Autenticación para saber cómo obtener una.

#LlamadaQué te ofrece
1GET /accounts/with-risk-statusLas cuentas en las que la clave puede operar, cada una con su fase, estado, saldo y equity. Toma accountId de la cuenta en la que se va a operar.
2GET /v2/markets?accountId=…Los mercados negociables con el precio, las comisiones y los límites de apalancamiento contra los que se comprueba una orden en esa cuenta. Toma id del mercado.
3POST /orders con type: "market"Abre la posición. amount es la reserva de cotización a gastar, leverage el multiplicador.
4GET /positions/{accountId}/activeLas posiciones abiertas. Se necesitan porque la respuesta de la orden no lleva ningún identificador de posición.
5POST /orders con type: "take" y triggerPrice: "0"Cierra esa posición a mercado. amount es el tamaño base a cerrar.

Tres cosas que resolver antes de escribir cualquier código:

  • Cada número es una cadena sin procesar fp9 — el valor escalado por 10⁹. 100 USD es "100000000000", el apalancamiento de 10x es "10000000000". Léelos y escríbelos con BigInt / Decimal; un float redondea silenciosamente los últimos dígitos.
  • amount cambia de unidad con la clase de orden. En la orden market que abre una posición, es un importe de cotización — margen, comisión, spread y buffer reservados del saldo libre. En la orden take que la cierra, es el tamaño del activo base de la posición, y no se reserva nada. El paso 5 pasa el size de la posición directamente.
  • Cerrar es una orden take, no una orden de mercado en la dirección contraria. Las posiciones se mantienen por dirección, así que una orden de mercado short colocada contra una long abierta abre una segunda posición en lugar de cerrar la primera. Una take con triggerPrice: "0" no lleva disparador y se ejecuta a mercado mientras la llamada sigue abierta; un amount mayor que la posición mantenida la cierra por completo. Para aplanar una cuenta en una sola llamada, usa POST /positions/{accountId}/close-all.

Ambos scripts a continuación están completos: establece UPSCALE_API_KEY en el entorno y ejecuta.

JavaScript (Node 18+)

Guarda como upscale.mjs y ejecuta con node upscale.mjs — sin dependencias.

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+)

Requiere 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')

A dónde ir a continuación

  • Las entradas diferidas (limit, stop_market, stop_limit) y las órdenes protectoras (stop, trailing_stop) toman la misma POST /orders ruta — la operación enumera lo que requiere cada tipo.
  • Un stop-loss y un take-profit pueden acompañar a la orden de apertura como stopTriggerPrice y takeTriggerPrice.
  • GET /accounts/{accountId}/risk-status es el endpoint que se debe consultar periódicamente para obtener el margen disponible de drawdown mientras una posición está abierta.

Autenticación

Crea una clave en la aplicación Upscale (o con POST /user/api-keys desde una sesión autenticada) — la clave sin procesar se muestra una sola vez, en el momento de la creación. Pásala como token de portador:

Authorization: Bearer usk_<your key>

Una clave hereda los permisos del usuario al que pertenece y permanece válida hasta que se elimina, se renueva o caduca.

El trading a través de la API se habilita por cuenta: una cuenta con api_trading desactivado responde 403 (api_trading_not_enabled) a cada solicitud realizada con una clave, y las listas de cuentas lo omiten por completo.

Convenciones

  • Los importes, precios, apalancamiento y multiplicadores viajan como fp9 cadenas de enteros sin procesar — el valor escalado por 10⁹. "1000000000" es 1 USD, "10000000000" es 10x apalancamiento, "50000000" es 5%.
  • fp9 fija la escala, no la unidad. Un importe de cotización es USD; un tamaño base son unidades del activo negociado. amount en una orden es cotización en órdenes de aumento y base en órdenes de cierre — cada campo dice cuál de los dos contiene.
  • Los identificadores son uuid v4. Los mercados se direccionan por identificador al operar y por el ticker del activo base (BTC) en los endpoints de lectura por mercado.
  • Las listas se paginan mediante limit y offset y devuelven el recuento total junto a la página.
  • Las posiciones y las órdenes están acotadas a la fase en la que se encuentra la cuenta en este momento: lo que pertenece a una fase finalizada ya no se devuelve.

Límites de velocidad

Los límites se cuentan por clave, en ventanas fijas, por separado para los endpoints de lectura (GET) y de escritura (todo lo demás). Los valores predeterminados son 10 solicitudes por segundo y 60 por minuto para lecturas, 10 por segundo y 60 por minuto para escrituras.

Superar un límite devuelve 429 (api_key_rate_limit_exceeded) con un encabezado Retry-After y un cuerpo que contiene el bucket (read / write), la ventana que se activó, su límite y retryAt.

Los límites propios de la clave son los únicos a los que está sujeta una clave. Los límites por endpoint con los que se topa la aplicación Upscale se cuentan por sesión interactiva y no se aplican a las solicitudes con clave de API, por lo que ningún endpoint aquí tiene un límite más estricto propio.

Errores

Un fallo responde con { statusCode, message, error }, donde error es un código estable legible por máquina. Bifurque según ese código, no según el texto del mensaje.

  • 400 — el cuerpo, la consulta o la ruta no superaron la validación del esquema (validation_failed), o se rechazó una precondición de trading (insufficient_balance, position_not_available, y los códigos por operación a continuación).
  • 401 — la clave falta, está malformada, revocada o caducada (session_expired).
  • 403 — la clave se usó en un endpoint que no acepta claves (api_key_not_allowed), la cuenta pertenece a otra persona (account_access_denied), el trading por API está desactivado (api_trading_not_enabled), la cuenta ya no opera (challenge_closed), no ha alcanzado una fase negociable (account_phase_not_allowed) o está bloqueada por el límite de capital gestionado (funded_limit_trading_locked), la orden ya no está activa (order_not_active), o el mercado está pausado (market_paused), solo cierre (market_close_only) o fuera de la categoría en la que la cuenta puede operar (market_category_not_allowed).
  • 404 — no existe tal cuenta (account_not_found), orden (order_not_found), posición (position_not_found) o mercado (market_not_found).
  • 409 — una solicitud con la misma x-idempotency-key todavía se está procesando (idempotency_key_in_flight), la cuenta no está cargada actualmente por el motor de trading (account_not_loaded), o el precio de mercado no se ha actualizado lo suficientemente recientemente como para operar con él (market_price_stale).
  • 429 — se superó el límite de velocidad de la clave (api_key_rate_limit_exceeded).

Los rechazos a nivel de orden se devuelven como 400 con un código que nombra la combinación de campos en conflicto; cada operación de trading enumera los que puede producir.

Operaciones

Last updated