Programar estrategias en Java

Emite señales de operación e información, configura órdenes y adjunta metadatos de gráfico.

Una estrategia de QTSurfer consume datos de mercado, actualiza indicadores y estado, y emite señales. Esta guía cubre la emisión de señales — el punto donde una observación se convierte en una instrucción de operar o en un dato para inspeccionar más tarde — y recibir un comando desde fuera de una ejecución en vivo.

Para la API completa de clases, usa el Javadoc del motor, en particular el paquete de señales de estrategia. Para redactar con ayuda de un agente, instala la skill mantenida qtsurfer-java-strategy:

npx skills add QTSurfer/strategy-skills --skill qtsurfer-java-strategy

La skill también cubre cómo elegir una clase base de estrategia, configurar indicadores y gestionar el estado por instrumento. Una vez listo el fuente, compílalo y valídalo a través de la API.

Los helpers de señal de esta guía no están atados a una clase Java: los cuerpos { } de una estrategia QTScript (beta) — una forma compacta de escribir una estrategia que deja fuera la clase, los imports y el listener — llaman a emitBuy, emitSell, emitInfo y emitSignal exactamente como se muestra aquí.

Señales de ejecución y señales de información

Estas familias de señales tienen efectos distintos:

SeñalPropósito¿Causa una operación?
BuySignalExpresa una instrucción de compra y su configuración de ordenSí
SellSignalExpresa una instrucción de venta y su configuración de ordenSí
InfoStrategySignalRegistra indicadores, diagnósticos o metadatos de visualizaciónNo

Una señal de información etiquetada BUY sigue siendo solo información. A la inversa, emitBuy(price) emite una señal de compra ejecutable aunque no lleve metadatos de gráfico adjuntos.

El ejemplo del README emite deliberadamente ambas. Publica una señal de información en cada actualización de ventana para poder inspeccionar la serie de indicadores, pero solo emite una compra o venta cuando cruzan las medias móviles:

InfoStrategySignal signal = createInfoSignal();
signal.set("fast", fast);
signal.set("slow", slow);

if (isBullish && !wasBullish) {
    emitBuy(price);
} else if (!isBullish && wasBullish) {
    emitSell(price);
}

emitSignal(signal);

Helpers de señal

Dentro de un AbstractWindowListener, el listener ya conoce su estrategia y su instrumento:

HelperResultado
emitBuy(price)Crea y emite de inmediato una BuySignal de mercado
emitSell(price)Crea y emite de inmediato una SellSignal de mercado
createBuySignal(price)Crea una señal de compra para personalizarla antes de emitirla
createSellSignal(price)Crea una señal de venta para personalizarla antes de emitirla
createInfoSignal()Crea una señal de información para rellenarla antes de emitirla
emitInfo(key, values...)Crea, rellena y emite de inmediato una señal de información
emitSignal(signal)Emite una señal creada o personalizada por el listener

A nivel de la clase de estrategia, los helpers de operación equivalentes reciben el instrumento de forma explícita: emitBuy(instrument, price), emitSell(instrument, price), createBuySignal(instrument, price) y createSellSignal(instrument, price). Usa createInfoStrategySignal(instrument) cuando construyas ahí una señal de información.

El price que se pasa a los helpers de operación inmediatos es el precio de referencia actual de la estrategia. La señal es market por defecto; cuando una señal cambia a limit, ese precio pasa a ser su precio límite.

Personalizar señales de compra y venta

Los helpers inmediatos aceptan deliberadamente solo un precio. Para configurar una orden, crea su señal, fija las opciones necesarias y emítela exactamente una vez:

import com.wualabs.qtsurfer.engine.exchange.trade.OrderFlag;
import com.wualabs.qtsurfer.engine.strategy.event.signal.BuySignal;
import com.wualabs.qtsurfer.engine.strategy.event.signal.MarketHintSignal.OrderKind;

BuySignal buy = createBuySignal(price);
buy.setOrderKind(OrderKind.limit);
buy.setMaxTries(3);
buy.setFlags(OrderFlag.GTC);
buy.set("reason", "ema-cross");
emitSignal(buy);

BuySignal y SellSignal heredan estas opciones de MarketHintSignal, la clase base común y referencia autorizada de sus métodos:

MétodoSignificado
setOrderKind(OrderKind.market)Orden a mercado; es el valor por defecto
setOrderKind(OrderKind.limit)Orden límite al price de la señal
setMaxTries(n)Número máximo de intentos para una compra límite; n debe ser positivo
setFlags(flags...)Indicadores de orden como FOK, IOC o GTC; el soporte real depende del exchange
setSellPercent(percent)Porcentaje de la posición a cerrar; pensado para ejecución multi-entrada, por defecto 100
setStopPrice(price)Stop de protección fijo que se arma tras ejecutarse la entrada
setStopLimitPrice(price)Precio límite opcional para ese stop fijo; sin él, el stop sale a mercado
setTrailPercent(percent)Stop de protección dinámico, expresado como porcentaje desde el extremo de precio favorable en curso
setStopCondition(condition)Predicado en vivo que condiciona un stop fijo o dinámico gestionado por el motor
set(key, values...)Metadatos arbitrarios de analítica, procedencia o visualización que viajan con la señal

Trata stop y stopTrailing como tipos de orden gestionados por el motor. El código de la estrategia debe expresar el riesgo de protección en la señal de entrada con setStopPrice o setTrailPercent, en lugar de emitir una orden de stop independiente.

Stops de protección

Una entrada en largo puede armar un stop fijo como parte de la misma señal:

BuySignal buy = createBuySignal(price);
buy.setStopPrice(price * 0.95);
emitSignal(buy);

Usa también setStopLimitPrice cuando la salida de protección deba ser stop-limit en lugar de stop-market. Un stop dinámico sigue el extremo favorable y se dispara tras el retroceso porcentual configurado:

BuySignal buy = createBuySignal(price);
buy.setTrailPercent(2.0);
emitSignal(buy);

Los mismos campos se aplican de forma simétrica a una entrada en corto. Una condición de stop la evalúa el motor de forma repetida y puede suprimir el stop hasta que una condición más amplia de la estrategia lo permita. Es lógica de estrategia en vivo, no datos de señal serializables.

Información y metadatos de gráfico

createInfoSignal() es azúcar sintáctico local al listener: crea una InfoStrategySignal ya vinculada a la estrategia y el instrumento actuales. Rellénala con set y emítela cuando esté lista:

InfoStrategySignal signal = createInfoSignal();
signal.set("price", price);
signal.set("fast", fast);
signal.set("slow", slow);
emitSignal(signal);

set almacena un valor directamente. Una lista par de pares nombre/valor crea un objeto anidado bajo la clave dada, que es por lo que los marcadores de gráfico usan esta forma:

signal.set("_m",
    "position", "belowBar",
    "shape", "arrowUp",
    "color", "#26a69a",
    "text", "BUY");

Las posiciones de marcador que usa la visualización estándar son aboveBar, belowBar e inBar; las formas portables son circle, arrowUp, arrowDown y square. Prefijar una propiedad con _ la reserva como metadato de control en lugar de una serie normal representada, tal como hace _m aquí.

Todo lo que haces set en una señal es su data, y se publica con la señal en una ejecución en vivo: quien pueda leer la ejecución puede leerlo, así que en una ejecución public es público. Una señal cuyo data pese más de 8 KiB (8.192 bytes de su JSON) no se empuja por el canal WebSocket; GET /live/{runId}/signals la devuelve entera de todos modos.

Para un único valor, emitInfo es la forma más corta:

emitInfo("zscore", zscore);

También acepta pares nombre/valor anidados:

emitInfo("averages", "fast", fast, "slow", slow);

emitInfo acepta los mismos argumentos en un cuerpo QTScript — el README muestra el ejemplo de media móvil de arriba con sus marcadores de gráfico escritos así.

Usa la forma más larga createInfoSignal() cuando un evento necesite varios valores de primer nivel o metadatos de marcador. Las señales de información son útiles para explicar una decisión, pero nunca sustituyen al emitBuy o emitSell correspondiente cuando la estrategia tiene que operar.

Recibir comandos

El dueño de una ejecución en vivo puede indicarle un comando desde fuera — POST /live/{runId}/commands — mientras sigue corriendo, sin reiniciarla. Para actuar sobre uno, implementa CommandRequestHandler:

import com.wualabs.qtsurfer.engine.strategy.event.request.CommandRequest;
import com.wualabs.qtsurfer.engine.strategy.event.request.CommandRequestHandler;

public class MyStrategy extends AbstractTickerStrategy implements CommandRequestHandler {

    @Override
    public void handle(CommandRequest request) {
        if ("flatten".equals(request.getCommand())) {
            // cierra la posición, cancela órdenes pendientes, lo que signifique "flatten" para esta estrategia
        }
    }
}

handle corre en el mismo hilo que update(), justo antes del evento de mercado al que apunta el comando, así que ve el estado de la estrategia exactamente como estaba en ese momento y puede llamar a cualquier cosa que pueda llamar update() — leer indicadores, emitir una señal, cambiar campos internos. Una RuntimeException que lance se captura y se cuenta, igual que una de update(); un Error desmonta la ejecución.

Un comando es siempre una cadena simple, y es transitorio. También puede llevar un objeto properties de tu elección, junto a command en el cuerpo de la petición — no params, que sigue siendo lo que fija una ejecución al arrancar y lo que cambia PUT /live/{runId}/params. Cada propiedad aterriza como una entrada de primer nivel en el propio mapa de CommandRequest, así que lee una directamente de request por su nombre — request.get("<clave>") — ninguna clave está prohibida, ya que el texto del comando se guarda aparte (getCommand() lo lee, sin verse afectado por nada de esto). Un valor conserva el tipo JSON con el que llegó, así que asignarlo a un campo String cuando quien llama envió un número o un objeto lanza un ClassCastException dentro de handle; un cuerpo onCommand de QTScript lee el mismo valor con $command.<clave> en su lugar, que siempre lo ensancha a un String (null para una clave ausente, nunca un fallo de conversión). Un comando, y sus propiedades, no se guardan como parte de la ejecución: una réplica que se reinicia reproduce solo el último tramo de datos de mercado, y un comando de antes de esa ventana simplemente no le llega.

Un comando no lleva instrumento asociado como sí lo hace update(); cuando sus propias propiedades nombran uno, alcanza el store de ese instrumento con getStateStore(String):

@Override
public void handle(CommandRequest request) {
    String instrument = request.get("instrument");
    if (instrument != null) {
        getStateStore(instrument).set("flattened");
    }
}

Ni un campo @StrategyProperty ni un StateStore escrito desde dentro de handle son duraderos. Ambos cambian de inmediato, en memoria, igual que cualquier otra asignación, pero ninguno se escribe en el conjunto de parámetros guardado de la ejecución — una réplica que se reinicia (o una que arranca después, y nunca corrió handle para ese comando) parte de lo último que fijó PUT /live/{runId}/params, no de lo que asignó un comando. La única escritura duradera es una llamada real a PUT /live/{runId}/params, desde fuera de la ejecución — una estrategia no puede llamar a su propia API REST desde dentro de handle.

Una ejecución cuya estrategia no implementa CommandRequestHandler responde todos los comandos con un 409 — implementar la interfaz es lo que hace que POST /live/{runId}/commands haga algo.

Una estrategia QTScript también la implementa, mediante su propia sección onCommand { } (consulta QTScript) — la plataforma reconoce la clase generada como CommandRequestHandler de la misma forma que reconoce esta.

Consulta Comandos para la forma de la petición/respuesta y los códigos de error.

Ver también

  • QTScript (beta) — las mismas estrategias escritas sin la ceremonia.
  • Estrategias — compila, valida, lista y recupera una estrategia, en cualquiera de los dos lenguajes.