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.
| # | Llamada | Qué te ofrece |
|---|---|---|
| 1 | GET /accounts/with-risk-status | Las 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. |
| 2 | GET /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. |
| 3 | POST /orders con type: "market" | Abre la posición. amount es la reserva de cotización a gastar, leverage el multiplicador. |
| 4 | GET /positions/{accountId}/active | Las posiciones abiertas. Se necesitan porque la respuesta de la orden no lleva ningún identificador de posición. |
| 5 | POST /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 conBigInt/Decimal; un float redondea silenciosamente los últimos dígitos. amountcambia de unidad con la clase de orden. En la ordenmarketque abre una posición, es un importe de cotización — margen, comisión, spread y buffer reservados del saldo libre. En la ordentakeque la cierra, es el tamaño del activo base de la posición, y no se reserva nada. El paso 5 pasa elsizede 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 mercadoshortcolocada contra unalongabierta abre una segunda posición en lugar de cerrar la primera. UnatakecontriggerPrice: "0"no lleva disparador y se ejecuta a mercado mientras la llamada sigue abierta; unamountmayor que la posición mantenida la cierra por completo. Para aplanar una cuenta en una sola llamada, usaPOST /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 mismaPOST /ordersruta — 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
stopTriggerPriceytakeTriggerPrice. GET /accounts/{accountId}/risk-statuses 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.
amounten 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
limityoffsety 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 mismax-idempotency-keytodaví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
-
GET /accounts/{accountId}/equity-history — Obtener el historial de capital de la cuenta del trader
-
GET /accounts/{accountId}/risk-status — Obtener el estado de riesgo de la cuenta del trader
-
GET /accounts/{accountId}/stats — Obtener estadísticas de trading de la cuenta del trader
-
POST /accounts/{accountId}/close-all — Cerrar todas las posiciones y órdenes
-
GET /accounts/events — Obtener los eventos de la cuenta del trader
-
GET /accounts/risk-status — Obtener el estado de riesgo de todas las cuentas del usuario actual.
-
GET /accounts/with-risk-status — Obtener todas las cuentas del usuario actual con estado de riesgo
-
POST /orders/{accountId}/close-all — Cerrar todas las órdenes
-
GET /orders/{accountId}/active — Obtener todas las órdenes activas
-
GET /orders/{accountId}/{asset}/active — Obtener órdenes activas por ticker
-
GET /orders/{accountId}/{asset}/history — Obtener el historial de órdenes por ticker
-
PATCH /orders/{orderId} — Cambiar el precio de activación de la orden
-
GET /positions/{accountId}/active — Obtener todas las posiciones activas
-
GET /positions/{accountId}/portfolio/history — Obtener el historial de todas las posiciones
-
GET /positions/{positionId}/history — Obtener todos los eventos por posición
-
GET /positions/{positionId} — Obtener detalles de la posición
-
PATCH /positions/{positionId}/margin — Cambiar el margen de la posición
-
POST /positions/{accountId}/close-all — Cerrar todas las posiciones
-
GET /positions/{accountId}/{asset}/history — Obtener el historial de posiciones por ticker
-
GET /positions/{accountId}/{asset}/active — Obtener las posiciones activas por ticker
-
GET /positions/{accountId}/{asset}/open-notional — Obtener el interés abierto actual por ticker