Blog
Development·12 min read

Construyendo Hyperscalper: Un Terminal de Trading Cripto 100% del Lado del Cliente

Deep-dive técnico en la construcción de un terminal de trading profesional para Hyperliquid DEX sin backend, múltiples métodos de entrada de órdenes y escáneres de mercado en tiempo real.

Jo Vinkenroye·January 13, 2026
Construyendo Hyperscalper: Un Terminal de Trading Cripto 100% del Lado del Cliente

Hyperscalper es un terminal profesional de trading de criptomonedas que construí para Hyperliquid DEX. Lo que lo hace único es que es 100% del lado del cliente - sin servidores backend, sin intermediarios. Tus claves privadas nunca salen de tu navegador.

Interfaz de Trading de Hyperscalper
Interfaz de Trading de Hyperscalper

En este post voy a recorrer las decisiones arquitectónicas clave, los diferentes métodos de entrada de órdenes que implementé, y cómo funcionan los escáneres de mercado en tiempo real.

¿Por Qué 100% del Lado del Cliente?

Cuando construyes una aplicación de trading que maneja claves privadas, la confianza lo es todo. Tomé una decisión deliberada: cero backend.

El Problema de la Confianza

Las plataformas de trading tradicionales requieren que confíes en ellas con tus credenciales. Incluso con las mejores prácticas de seguridad, siempre hay un servidor que podría ser comprometido, una base de datos que podría ser vulnerada, o un empleado que podría volverse deshonesto.

Con Hyperscalper, el flujo de datos es simple:

Browser → Hyperliquid DEX API

Eso es todo. Sin servidores relay, sin proxies, sin bases de datos backend.

Cómo se Aseguran las Claves Privadas

Aunque todo corre en el navegador, igual necesitaba encriptación robusta para la clave privada almacenada en localStorage. Este es el enfoque:

// AES-GCM encryption with PBKDF2 key derivation
Const deriveKey = async (password: string, salt: Uint8Array) => {
const keyMaterial = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations: 100000, // Makes brute-force expensive
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
};

El sistema genera una clave específica del dispositivo en el primer uso, almacenada en localStorage. Los usuarios pueden opcionalmente agregar una contraseña para protección adicional. Incluso si alguien roba los datos de localStorage, necesitaría 100,000 iteraciones de PBKDF2 para crackear cada intento.

Los Trade-offs

Ir 100% del lado del cliente tiene consecuencias:

  • Cero confianza requerida — pero no puedes correr bots del lado del servidor
  • Sin recolección de datos — pero el historial de órdenes depende de la API del exchange
  • Verdadera descentralización — pero toda la computación ocurre en el navegador
  • Sin costos de servidor — pero no puedes hacer backtesting pesado

Para un terminal de scalping donde la velocidad importa, estos trade-offs tienen sentido. La latencia entre tu navegador y Hyperliquid es todo lo que importa.

Métodos de Entrada de Órdenes

Los traders tienen diferentes flujos de trabajo. Algunos prefieren hacer clic, otros usan hotkeys, y algunos quieren órdenes ladder automatizadas. Implementé cinco formas distintas de ingresar órdenes.

Órdenes Click-on-Chart

A veces quieres colocar una orden a un nivel de precio específico que ves en el gráfico. Implementé un sistema de crosshair que hace esto intuitivo:

Colocación Precisa de Órdenes
Colocación Precisa de Órdenes
Const handleChartClick = (params) => {
if (!crosshairActive) return;
const price = params.price;
const isBuy = price < currentPrice;
placeLimitOrderAtPrice({
symbol,
price,
isBuy,
percentage: settings.orderSizePercent
});
setCrosshairActive(false);
};

Clic debajo del precio actual = orden límite de compra. Clic arriba = orden límite de venta. Simple e intuitivo.

Cloud Ladder Orders (DCA)

La función estrella. En lugar de colocar una sola orden, las Cloud orders colocan 5 órdenes límite apiladas a intervalos por debajo (para compras) o por encima (para ventas) del precio actual.

Const buyCloud = async ({ symbol, currentPrice, priceInterval, percentage }) => {
const ORDER_COUNT = 5;
const TAKE_PROFIT_PERCENT = 2;
const orders = [];
for (let I = 0; I < ORDER_COUNT; I++) {
const price = currentPrice - (priceInterval * I);
const size = totalSize / ORDER_COUNT;
orders.push({
type: 'limit',
side: 'buy',
price,
size,
reduceOnly: false
});
// Auto TP at 2% profit
orders.push({
type: 'limit',
side: 'sell',
price: price * (1 + TAKE_PROFIT_PERCENT / 100),
size,
reduceOnly: true
});
}
await Promise.all(orders.map(o => submitOrder(o)));
};

El intervalo de precio se calcula a partir de las alturas de las velas recientes - el sistema se adapta a la volatilidad actual.

Atajos de Teclado

Para traders de velocidad, mover la mano al ratón es demasiado lento. Mapeé todas las acciones comunes a hotkeys:

Atajos de Teclado
Atajos de Teclado
UseEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.metaKey || e.ctrlKey) {
switch (e.key) {
case 'b':
e.preventDefault();
inverted ? SellCloud() : buyCloud();
break;
case 's':
e.preventDefault();
inverted ? BuyCloud() : sellCloud();
break;
case 'e':
e.preventDefault();
closePosition(25); // Close 25%
break;
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [inverted]);

Fíjate en el check de inverted - más sobre eso después.

Actualizaciones Optimistas

Nadie quiere esperar 500ms por una respuesta de API antes de ver feedback. Implementé actualizaciones optimistas:

Const placeOrder = async (order) => {
// 1. Immediately show in UI
const optimisticId = crypto.randomUUID();
addOptimisticOrder({ ...order, id: optimisticId });
showToast('Placing order...');
try {
// 2. Actually submit to exchange
const result = await submitToHyperliquid(order);
// 3. Replace optimistic with real
removeOptimisticOrder(optimisticId);
addConfirmedOrder(result);
playSuccessSound();
} catch (error) {
// 4. Rollback on failure
removeOptimisticOrder(optimisticId);
showErrorToast(error.message);
}
};

La UI responde en menos de 100ms sin importar la latencia de red.

Escáneres de Mercado

Un terminal de trading es tan bueno como su capacidad para encontrar oportunidades. Construí 8 tipos diferentes de escáneres que corren en tiempo real a través de todos los símbolos.

Resultados del Escáner
Resultados del Escáner

Arquitectura de los Escáneres

Cada escáner se suscribe a datos de velas vía WebSocket y ejecuta análisis en cada actualización:

// Shared WebSocket connection (singleton pattern)
Const ws = getWebSocketService();
// Subscribe to all symbols on multiple timeframes
Symbols.forEach(symbol => {
['1m', '5m', '15m', '1h'].forEach(timeframe => {
ws.subscribeToCandles(symbol, timeframe, (candles) => {
runScanners(symbol, timeframe, candles);
});
});
});

Escáner Estocástico

El oscilador estocástico es genial para encontrar condiciones de sobreventa/sobrecompra. Implementé cuatro variantes con diferentes períodos:

Const scanStochastic = (candles, config) => {
const variants = {
ultraFast: { period: 5, smoothK: 2, smoothD: 2 },
fast: { period: 9, smoothK: 3, smoothD: 3 },
medium: { period: 14, smoothK: 3, smoothD: 3 },
slow: { period: 21, smoothK: 5, smoothD: 5 }
};
const signals = [];
Object.entries(variants).forEach(([name, params]) => {
if (!config.variants[name].enabled) return;
const stoch = calculateStochastic(candles, params);
const current = stoch[stoch.length - 1];
const previous = stoch[stoch.length - 2];
// Bullish: K crosses above D in oversold zone
if (current.k > current.d &&
previous.k <= previous.d &&
current.k < config.oversoldLevel) {
signals.push({
type: 'stochastic',
variant: name,
direction: 'bullish',
strength: (config.oversoldLevel - current.k) / config.oversoldLevel
});
}
});
return signals;
};

Detección de Divergencias

Las divergencias son señales poderosas - cuando el precio hace un nuevo mínimo pero el RSI hace un mínimo más alto, frecuentemente señala una reversión. Esto requirió lógica más compleja:

Const detectDivergence = (candles, rsiValues) => {
const pricePivots = findPivots(candles, 3);
const rsiPivots = findPivots(rsiValues, 3);
const divergences = [];
const recentPriceLows = pricePivots
.filter(p => p.type === 'low')
.slice(-5);
for (let I = 1; I < recentPriceLows.length; I++) {
const current = recentPriceLows[I];
const previous = recentPriceLows[I - 1];
// Price made lower low
if (current.price < previous.price) {
const currentRsi = findClosestPivot(rsiPivots, current.index);
const previousRsi = findClosestPivot(rsiPivots, previous.index);
// RSI made higher low = bullish divergence
if (currentRsi.value > previousRsi.value) {
divergences.push({
type: 'regular_bullish',
pricePoints: [previous, current],
rsiPoints: [previousRsi, currentRsi],
strength: calculateDivergenceStrength(...)
});
}
}
}
return divergences;
};

Consideraciones de Rendimiento

Correr 8 escáneres a través de 500+ símbolos en 4 timeframes podría fácilmente matar el rendimiento del navegador. Usé varias técnicas de optimización:

Memoización con TTL:

Const memoizedCalculate = createMemoizedFunction(
calculateStochastic,
(candles, period) => `stoch-${candles.length}-${period}`,
100, // max cache entries
60000 // expire after 60s
);

Detección de divergencias con debounce:

// Don't recalculate on every tick
Const debouncedDivergence = useDebouncedCallback(
detectDivergence,
1000 // Wait 1s after last update
);

Virtual scrolling para resultados:

<FixedSizeList
height={600}
itemCount={signals.length}
itemSize={60}
>
{({ index, style }) => (
<SignalItem signal={signals[index]} style={style} />
)}
</FixedSizeList>

Análisis Multi-Temporal

Una de las funciones más potentes es la vista multi-temporal. Ver el mismo símbolo en 1m, 5m, 15m y 1h simultáneamente ayuda a identificar confluencia.

Vista Multi-Temporal
Vista Multi-Temporal

Todos los gráficos están sincronizados - cuando haces zoom o paneas un gráfico, todos los demás siguen. Esto se logra con un store de sincronización compartido:

Const useChartSyncStore = create((set) => ({
timeRange: null,
setTimeRange: (range) => set({ timeRange: range }),
}));
// In each chart component
UseEffect(() => {
const unsubscribe = useChartSyncStore.subscribe(
(state) => state.timeRange,
(range) => {
if (range) chart.timeScale().setVisibleRange(range);
}
);
return unsubscribe;
}, [chart]);

Líneas de Soporte y Resistencia

La detección automatizada de líneas de tendencia es una de esas funciones que suena simple pero tiene una profundidad sorprendente. Implementé tres enfoques complementarios.

Líneas de Tendencia Basadas en Pivotes

El enfoque más intuitivo: encontrar puntos pivote y trazar líneas a través de ellos.

Const detectPivots = (candles, strength = 3) => {
const pivots = [];
for (let I = strength; I < candles.length - strength; I++) {
const current = candles[I];
let isHigh = true;
for (let j = I - strength; j <= I + strength; j++) {
if (j !== I && candles[j].high >= current.high) {
isHigh = false;
break;
}
}
if (isHigh) {
pivots.push({ index: I, price: current.high, type: 'high' });
}
// Same logic for lows...
}
return pivots;
};

Validación por Envolvente

Una línea de soporte que se viola no es útil. Valido las líneas contra todas las velas:

Const findBestSupportLine = (pivots, candles) => {
let bestLine = null;
let bestScore = -Infinity;
for (let I = 0; I < pivots.length - 1; I++) {
for (let j = I + 1; j < pivots.length; j++) {
const line = createLine(pivots[I], pivots[j]);
let touches = 0;
let violations = 0;
candles.forEach((candle, idx) => {
const linePrice = getLineValueAt(line, idx);
const tolerance = linePrice * 0.002;
if (candle.low < linePrice - tolerance) {
violations++;
} else if (Math.abs(candle.low - linePrice) < tolerance) {
touches++;
}
});
const score = touches * 10 - violations * 100;
if (score > bestScore && violations === 0 && touches >= 3) {
bestScore = score;
bestLine = line;
}
}
}
return bestLine;
};

La clave: una línea de soporte con cero violaciones es mucho más valiosa que una con muchos toques pero algunas violaciones.

Analíticas de Trading

Hacer seguimiento del rendimiento es esencial para mejorar como trader. Hyperscalper provee resúmenes diarios y mensuales de P&L:

Resumen Diario
Resumen Diario
Resumen Mensual
Resumen Mensual

Todos estos datos vienen directamente de la API de Hyperliquid - sin necesidad de almacenamiento backend.

El Modo Invertido

Aquí va una función nacida de la experiencia real de trading: modo invertido.

Cuando eres un trader con sesgo bajista, la UI estándar es confusa. Verde significa que el precio subió (malo para shorts), rojo significa que bajó (bueno para shorts). Las señales alcistas son oportunidades bajistas.

El modo invertido voltea todo:

Const getSignalColor = (signal, inverted) => {
const isBullish = signal.direction === 'bullish';
if (inverted) {
// Bullish signal = shorting opportunity = show as "good" (green)
return isBullish ? Colors.bearish : colors.bullish;
}
return isBullish ? Colors.bullish : colors.bearish;
};

Colores de velas, indicadores de señales, incluso la semántica de las etiquetas de soporte/resistencia se invierten. Un trader de shorts ve los mismos patrones que un trader de longs, solo interpretados correctamente para su sesgo.

Soporte Multi-Monitor

Los traders profesionales frecuentemente usan múltiples monitores. Hyperscalper soporta separar gráficos en ventanas independientes:

Configuración Multi-Monitor
Configuración Multi-Monitor

Cada ventana emergente mantiene su propia conexión WebSocket y sincroniza estado con la ventana principal vía BroadcastChannel:

Const channel = new BroadcastChannel('hyperscalper-sync');
// Main window broadcasts state changes
Channel.postMessage({ type: 'POSITION_UPDATE', data: positions });
// Popup windows listen
Channel.onmessage = (event) => {
if (event.data.type === 'POSITION_UPDATE') {
setPositions(event.data.data);
}
};

Manejo de Estado con Zustand

Con 20+ stores manejando diferentes dominios, necesitaba manejo de estado liviano. Redux se sentía excesivo. Elegí Zustand:

Const useTradingStore = create<TradingStore>((set, get) => ({
orders: [],
positions: [],
addOrder: (order) => set((state) => ({
orders: [...state.orders, order]
})),
closePosition: async (symbol, percentage) => {
const position = get().positions.find(p => p.symbol === symbol);
if (!position) return;
const size = position.size * (percentage / 100);
await submitMarketOrder({
symbol,
side: position.side === 'long' ? 'sell' : 'buy',
size,
reduceOnly: true
});
}
}));

Cada store se enfoca en un dominio: trading, órdenes, posiciones, velas, escáner, configuración, etc. Los componentes se suscriben exactamente a lo que necesitan.

Lecciones Aprendidas

Construir Hyperscalper me enseñó varias cosas:

1. Del lado del cliente puede ser suficiente. Para aplicaciones donde la confianza importa, eliminar el backend no solo es posible - es preferible.

2. Las actualizaciones optimistas son esenciales. En trading, 500ms de latencia se sienten como una eternidad. Muestra feedback inmediatamente, reconcilia después.

3. Múltiples métodos de entrada importan. Diferentes traders tienen diferentes flujos de trabajo. Algunos hacen clic, otros usan hotkeys, algunos quieren automatización. Soporta a todos.

4. La optimización de rendimiento es obligatoria. Correr análisis técnico en 500 símbolos en tiempo real requiere atención cuidadosa a memoización, debouncing y renderizado virtual.

5. Pensar invertido ayuda. Construir para traders de shorts me obligó a cuestionar suposiciones sobre qué significan "arriba" y "bueno". El resultado es un sistema más flexible.

Hyperscalper está en vivo en hyperscalper.vercel.app si quieres probarlo. El código demuestra que herramientas de trading de nivel profesional pueden correr enteramente en el navegador - sin backend requerido.

Stay Updated

Get notified about new posts on automation, productivity tips, indie hacking, and web3.

No spam, ever. Unsubscribe anytime.

Comments

Related Posts