Un nuevo contrato aparece en el exchange, los primeros minutos el precio se mueve en todas direcciones y, después, parte del mercado recuerda que existe el control de riesgo. Esa ventana corta justo después del listado es el foco de este caso.

La tarea era típica para cualquier producto con lógica de mercado: formalizar la hipótesis, recoger los datos, construir un simulador, recorrer los parámetros y comprobar si existe una ventaja estable. En este caso, se trata de operaciones en corto en listados confirmados de Bybit, con riesgo fijo y reglas claras de TP/SL.

Si una estrategia no sobrevive a un backtest sencillo, es poco probable que sobreviva en un entorno donde las decisiones se mueven a velocidad de producto y no de presentación.

Para la prueba utilicé diez eventos de listado confirmados de contratos en USDT. Para cada uno cargué un intervalo de datos de velas de un minuto: desde 18 horas antes del listado hasta 10 horas después. La parte previa al listado se conserva como contexto para el análisis de patrones en la siguiente iteración; la parte posterior se usa directamente en el simulador de operaciones. Los datos se obtuvieron a través del endpoint Bybit V5 Market Kline, que devuelve velas históricas para el símbolo, intervalo y rango temporal solicitados. [web:133][web:172]

El momento de listado se define como entry_dt, mientras que el marco de datos se delimita mediante start_dt y end_dt. De este modo el modelo dispone de contexto antes del evento y de un tramo completo después del mismo.

Code Ventana de datos alrededor del listado
for symbol, entry_str in allpairs:
    entry_dt = parse_entry_dt(entry_str)
    start_dt = entry_dt - timedelta(hours=18)
    end_dt = entry_dt + timedelta(hours=10)

    df = get_klines(symbol, start_dt, end_dt, category="linear", interval="1")

La función que carga las velas convierte los timestamps en milisegundos, llama al API y devuelve un DataFrame ordenado. No hay datos sintéticos: solo lo que realmente existe en el histórico del instrumento. [web:133][web:172]

Code Obtención de velas históricas (kline)
def get_klines(symbol, start_dt, end_dt, category="linear", interval="1"):
    start_ts = int(start_dt.timestamp() * 1000)
    end_ts = int(end_dt.timestamp() * 1000)

    resp = session.get_kline(
        category=category,
        symbol=symbol,
        interval=interval,
        start=start_ts,
        end=end_ts,
        limit=1000
    )

    rows = resp["result"]["list"]
    df = pd.DataFrame(
        rows,
        columns=["timestamp", "open", "high", "low", "close", "volume", "turnover"]
    )
    df["datetime"] = pd.to_datetime(df["timestamp"].astype("int64"), unit="ms", utc=True)
    return df.sort_values("datetime").reset_index(drop=True)

El siguiente bloque es el simulador de operaciones. La entrada comienza en la primera vela en o después de entry_dt, y a partir de ahí se evalúan TP, SL y timeout en secuencia.

La estructura es deliberadamente minimalista: un único punto de entrada, tamaño de riesgo fijo y un recorrido lineal de las velas hasta que se active el TP, el SL o termine el escenario. Esto facilita la interpretación de resultados y mantiene la lógica visible, sin capas innecesarias.

Code Punto de entrada en corto
mask = df["datetime"] >= entry_dt
if not mask.any():
    return None

entry_idx = mask.idxmax()
entry_price = float(df.loc[entry_idx, "close"])
Code Simulación básica de una operación corta
def simulate_short_trade(df, entry_dt, entry_amount=100.0,
                         tp_pct=5.0, sl_pct=4.0, duration_min=600):
    mask = df["datetime"] >= entry_dt
    if not mask.any():
        return None

    entry_idx = mask.idxmax()
    entry_price = float(df.loc[entry_idx, "close"])
    pos_qty = entry_amount / entry_price

    tp_price = entry_price * (1 - tp_pct / 100.0)
    sl_price = entry_price * (1 + sl_pct / 100.0)
    end_idx = min(entry_idx + duration_min, len(df) - 1)

    for i in range(entry_idx + 1, end_idx + 1):
        high_i = float(df.loc[i, "high"])
        low_i = float(df.loc[i, "low"])

        if low_i <= tp_price:
            return {"PnL": (entry_price - tp_price) * pos_qty,
                    "closure_type": "TP"}
        if high_i >= sl_price:
            return {"PnL": (entry_price - sl_price) * pos_qty,
                    "closure_type": "STOP"}

    last_close = float(df.loc[end_idx, "close"])
    return {"PnL": (entry_price - last_close) * pos_qty,
            "closure_type": "TIMEOUT"}

Sobre esta base se añadió una versión extendida con varios niveles de TP, salidas parciales y promedios en contra del movimiento. En productos donde la gestión de posición necesita más flexibilidad, estos son los bloques que se sustituyen primero.

La siguiente capa es la selección de parámetros. En lugar de discutir si un TP del 5% «suena mejor» que uno del 4%, se ejecutó un grid search: varios valores para el primer y segundo TP, distintos niveles de stop loss y diferentes estructuras de distribución de la posición, desde salida total hasta escenarios fraccionados.

El enfoque es el habitual en cualquier módulo de backtesting: definir la cuadrícula de parámetros, ejecutarla sobre la misma muestra y, solo entonces, decidir qué merece pasar al entorno de producción.

Code Grid search para TP/SL y TP‑shares
for tp1, tp2, stop, tp_sh in product(tp1_grid, tp2_grid,
                                     stop_grid, tp_shares_grid):
    tplevels = [tp1, tp2, tp3]

    trades_df_g, total_pnl_g, mean_pnl_g, win_rate_g = run_backtest(
        allpairs=allpairs,
        data_cache=data_cache,
        entry_amount=100.0,
        tplevels=tplevels,
        tpshares=tp_sh,
        stoplosspct=stop,
        avglevels=2,
        avgpct=5.0,
        avgamount=50.0,
        duration_min=600,
    )
Si el producto necesita un «modo experimento», este es el lugar natural para conectar cuadrículas alternativas de parámetros y compararlas en nuevos lotes de listados.

En la muestra de 10 listados, la mejor configuración fue sorprendentemente simple: TP levels = [5, 6, 12], TP shares = [1.0, 0.0, 0.0] y stop loss = 4%. Es decir, cerrar toda la posición en el primer TP funcionó mejor que cualquier estructura fraccionada del test.

Las cifras finales para este setup: resultado total de 14.95 USD con un riesgo fijo de 100 USD por operación, beneficio medio de 1.50 USD por trade, win rate del 60% y una distribución de cierres de 6 TP, 4 SL y 0 timeouts. En una muestra pequeña son números modestos, pero bastan para distinguir una hipótesis viable de una reacción puramente emocional.

Code Resumen de la mejor configuración
print("FINAL SUMMARY")
print("Best TP levels:", best_tplevels)
print("Best TP shares:", best_tpshares)
print("Best stop loss:", best_stop)
print("Trades:", len(best_trades_df))
print("Total PnL:", round(best_total_pnl, 2))
print("Mean PnL:", round(best_mean_pnl, 2))
print("Win rate:", round(best_win_rate * 100, 2), "%")

La hora de entrada se analizó por separado. La franja más débil de esta muestra fue las 04:00 UTC, mientras que la más consistentemente positiva fue las 06:00 UTC. Para un producto, esto aporta un mensaje sencillo pero útil: el tiempo es un candidato válido para un filtro de riesgo adicional, no solo un parámetro decorativo.

Code Estadística por horas de entrada
hour_stats = (
    analysis_time
    .groupby("entry_hour")
    .agg(
        trades=("PnL", "count"),
        total_pnl=("PnL", "sum"),
        mean_pnl=("PnL", "mean"),
        winrate=("PnL", lambda x: (x > 0).mean())
    )
    .sort_values("entry_hour")
)

Qué aporta esto a un producto

Desde el punto de vista de arquitectura de solución, el resultado se resume así: la lógica básica de cortos funciona con TP del 5% y SL del 4%, salida total en el primer TP y un filtro de tiempo que favorece las 06:00 UTC y recomienda prudencia en torno a las 04:00 UTC. Es un conjunto de reglas transparente, fácil de trasladar a un producto, monitorizar en una muestra mayor y adaptar a otros universos de instrumentos.

Para equipos que construyen estrategias o servicios sobre el mercado, el valor no está solo en este setup concreto, sino en el enfoque: hipótesis explícita, vínculo claro con los datos del exchange, backtest con cuadrícula de parámetros controlada y métricas legibles al final. Los instrumentos, los horizontes y el universo pueden cambiar; el marco de validación, no.

  • Base: simulador de operaciones cortas sobre velas históricas de Bybit con control explícito de TP/SL.
  • Conclusión: la salida total en el primer TP se comportó mejor que las salidas fraccionadas en la muestra de listados analizada.
Probar una hipótesis de forma formal no garantiza una estrategia perfecta. Garantiza otra cosa: dejas de pagar al mercado por decisiones que se podían filtrar ya en la fase de notebook.