QTScript (beta)

Un lenguaje compacto para escribir estrategias — cada sección, las ventanas, qué hay en su ámbito, y cómo compila igual que Java.

QTScript es una forma compacta de escribir una estrategia: conservas la parte que es tuya — indicadores, ventanas, señales — y dejas fuera la ceremonia de una clase Java (paquete, imports, clase, clase base, anotaciones de propiedades, código repetitivo de listeners). Cada cuerpo { } es Java tal cual, así que todo lo que ofrece la API de estrategias Java dentro de un cuerpo funciona sin cambios. Un fichero QTScript se convierte en exactamente una clase Java y pasa por la misma compilación en el servidor que una estrategia Java.

Está en beta y ganará más sintaxis. Java sigue siendo la vía establecida, con toda la API del motor; lo que QTScript no puede expresar se escribe en Java (consulta qué no hace).

Se envía como cualquier estrategia

No hay un endpoint aparte ni una cabecera que fijar: envía el fuente en crudo a POST /strategy con Content-Type: text/plain, exactamente igual que con Java. La plataforma distingue ambas por el texto — un fuente QTScript empieza por strategy, y los espacios en blanco o comentarios (//, /* */) antes de esa palabra se ignoran, así que puede haber una descripción encima del fichero. .qtscript es la extensión de fichero convencional; solo se envía el texto.

curl -X POST https://api.qtsurfer.net/v1/strategy 
  -H "Authorization: Bearer $TOKEN" 
  -H "Content-Type: text/plain" 
  --data-binary @rsi-reversion.qtscript

La respuesta es la misma que para Java: un strategyId y los declaredProperties — cada param que declaraste aparece ahí.

Una estrategia entera

Este es el fichero completo: sin imports, sin clase, sin listener.

strategy "RSI reversion"

param rsiBajo = 30  "Sobrevendido"
param rsiAlto = 70  "Sobrecomprado"

instruments */usdt

setup:
  rsi(14) window m1 {
    if (actual < rsiBajo) emitBuy(price);
    if (actual > rsiAlto) emitSell(price);
  }

Declara dos parámetros, acepta cualquier base cotizada en USDT, registra un RSI de 14 periodos y lo vigila en una ventana de un minuto, emitiendo una señal en cada umbral.

Las partes de un fichero

ParteQué haceEjemplo
strategyLa primera línea: el nombre y, opcionalmente, la fuente de datos (ticker por defecto, kline, funding)strategy "RSI reversion" · strategy Barras kline
paramUn valor configurable. El tipo sale del literal (9 → int, 0.5 → double, true → boolean, "texto" → String). El nombre es la clave que usan los params de una ejecución y un eje de barridoparam rsiBajo = 30 "Sobrevendido"
init { }Opcional. Java tal cual, se ejecuta una vez al construir la estrategia — setters del motorinit { setPercentGain(0.5); }
instrumentsOpcional. Qué mercados: pares (BASE/QUOTE, cualquiera de los dos lados puede ser *), expresiones regulares (~"...", comparadas contra el símbolo completo), o un cuerpo Java para control totalinstruments */usdt · instruments btc/usdt, eth/*
setup:Los indicadores, una llamada al builder por línea (el mismo catálogo que usa una estrategia Java). Las líneas bajo setup: van indentadas, y la primera línea que vuelve a la columna 0 cierra la secciónema(12) · bollinger(20, 2)
VentanasDonde va la lógica — consulta abajorsi(14) window m1 { ... }
onCommand { }Opcional, como mucho una por fichero. Corre cuando la ejecución recibe un comando — consulta abajoonCommand { if ("flatten".equals($command)) ... }

Ventanas

Una ventana se dispara cuando cierra su periodo, no en cada tick, y envuelve un indicador. Un periodo es uno de s1 s5 s10 s30 m1 m3 m5, o un número entero de segundos (window 900 { ... }); si se omite, es s1. Cinco formas de escribir una:

setup:
  rsi(14) window m1 { ... }      // en línea, sobre el indicador que registra esta línea
  rsi(33) window Sobrevendido    // igual, con el cuerpo en una sección con nombre más abajo
  window price m5 { ... }        // en línea, sobre un indicador por nombre
  window ema12 Tendencia         // por nombre, cuerpo en una sección con nombre

window Sobrevendido m1 { ... }   // una sección con nombre, en la columna 0
Tendencia s5 { ... }             // `window` es opcional aquí

Una sección llamada Main a la que nada hace referencia vigila el valor principal (price, o rate en funding), lo que da el fichero útil más corto:

strategy Simple

Main m1 {
  if (actual > prev) emitBuy(price);
}

En qué indicador está una ventana

Dentro del cuerpo de cualquier ventana, $indicator es el nombre del indicador al que está enganchada esa ventana, como un String: "ema12" para una ventana escrita en la línea ema(12), "price" en un Main ("rate" en funding). El $ marca un nombre que aporta QTScript; los nombres que declaras tú no pueden empezar por uno. Una sección con nombre enganchada a varios indicadores ve, cada vez que se dispara, el indicador que la disparó.

También es la forma en que un cuerpo llega a un indicador que la misma llamada al builder registró junto al que ocupa la ventana. bollinger(20, 2) registra tres bajo un mismo nombre: la banda media, blgr20_2, y las bandas exteriores, ese nombre seguido de Upper y Lower. Una ventana escrita en esa línea se engancha a la banda media, así que actual es el valor de la banda media, y value($indicator + "Upper") y value($indicator + "Lower") leen las otras dos:

setup:
  bollinger(20, 2) window m1 {
    if (price > value($indicator + "Upper")) emitSell(price);
    if (price < value($indicator + "Lower")) emitBuy(price);
  }

Una ventana que nombra un indicador que no está registrado, como window noexiste m1 { ... }, no se rechaza al registrar el fuente. Que el nombre exista puede depender de tus param (un param rapida usado como ema(rapida) registra un nombre distinto para cada valor) y de indicadores registrados desde Java, así que solo se descubre al validar la estrategia, que entonces falla en la línea de la ventana: QTScript line 4: unknown indicator 'noexiste'.

Dentro de un cuerpo

Tu Java, más lo que ya está en ámbito — nada necesita importarse:

En ámbitoQué es
actual, prevel valor nuevo y el anterior de la ventana
price (ticker) · price open high low close volume (kline) · rate (funding)los valores actuales, como variables normales
value("nombre")el valor actual de cualquier otro indicador
$indicatorel nombre del indicador en el que está esta ventana, como un String — ver arriba
storeel estado por instrumento que comparten todas las ventanas de ese instrumento
emitBuy(price), emitSell(price), emitInfo(clave, valores…), emitSignal(signal)emisión de señales
cada paramlegible por su nombre

Un flag de posición en el store, emitiendo solo una vez en el cruce:

strategy "EMA cross"

param rapida = 9  "EMA rápida"
param lenta  = 21 "EMA lenta"

setup:
  ema(rapida)
  ema(lenta)
  window price s5 {
    double f = value("ema" + rapida);
    double s = value("ema" + lenta);
    if (f > s && !store.is("largo")) {
      store.set("largo");
      emitBuy(price);
    }
    if (f < s && store.is("largo")) {
      store.unset("largo");
      emitSell(price);
    }
  }

value("ema" + rapida) lee cualquier indicador registrado por nombre; store es el mismo store que comparte cada ventana del instrumento.

Velas

strategy … kline da los campos de la barra como variables normales. Una estrategia kline nunca recibe un intervalo: el ancho de la barra es la cadence con la que preparas los datos, así que el mismo fichero corre a cualquiera de ellas.

strategy "Range breakout" kline

param rangoMinimo = 0.5 "Rango mínimo, en porcentaje"

setup:
  window close m1 {
    double rango = (high - low) / low * 100.0;
    if (rango < rangoMinimo) return;
    if (close > open) emitBuy(close);
    else              emitSell(close);
  }

Manejar un comando

onCommand { } es una sección especial, como mucho una por fichero, que corre cuando la plataforma entrega un comando a una ejecución en vivo — POST /live/{runId}/commands, aplicado sin reiniciar la estrategia. No tiene periodo ni indicador: no es una ventana, corre una vez por comando, no una vez por evento de mercado, y un fichero tiene como mucho una, igual que tiene como mucho un init { }.

Dentro de su cuerpo, $command es el texto del comando, como un String — nada más de lo que tiene el cuerpo de una ventana (actual, $indicator, value(...), el store ambiental) está en ámbito, porque un comando no está atado a un tick de mercado ni, a diferencia de una ventana, a un instrumento ya elegido por ti. Cada param sigue siendo legible y asignable.

Un comando puede llevar un objeto properties de tu elección, junto a command en el cuerpo de la petición. Lee un valor de ahí con $command.<clave> — un String, null cuando el comando no llevaba esa clave. $command.<clave> solo se dispara cuando <clave> no es en sí misma una llamada, así que $command.equals(...), $command.startsWith(...) y el resto se siguen leyendo como métodos normales de String sobre el propio $command.

Al cuerpo de una ventana se le entrega el store de su instrumento; a onCommand no, ya que un comando no nombra ningún instrumento por sí mismo — pero getStateStore("<símbolo>") acepta uno directamente, así que un comando cuyas propias properties nombran un instrumento aún puede alcanzar el store de ese instrumento:

strategy "Manual flatten"

onCommand {
  if ("flatten".equals($command)) {
    getStateStore($command.instrument).set("cerrada");
  }
}

Una estrategia sin onCommand { } no implementa CommandRequestHandler del motor, así que un comando enviado a una de sus ejecuciones se rechaza con 409 (consulta Comandos). $command y $command.<clave> no son visibles fuera del cuerpo de onCommand, igual que $indicator no es visible fuera de una ventana.

Fijar un param o escribir en un StateStore desde dentro de onCommand surten efecto de inmediato, pero ninguno de los dos sobrevive a un reinicio: un StateStore es memoria, se pierde en un reinicio igual que un campo. Solo PUT /live/{runId}/params escribe algo de lo que una réplica reiniciada realmente parte. getStateStore(...) aquí no trata de durabilidad — es cómo onCommand alcanza el estado por instrumento que el cuerpo de una ventana ya lee, ya que un comando no lleva instrumento propio.

Una estrategia Java implementa el mismo contrato directamente, mediante CommandRequestHandler — consulta Programar estrategias en Java.

Ejecutarla

Una estrategia QTScript registrada se prepara, ejecuta y barre como cualquier otra: prepare, execute y executeSweep, con el strategyId que recibiste. Los ejes de barrido y params usan los nombres de tus líneas param.

strategy …PrepareExecuteSweep
ticker (por defecto)sísísí
klinesísísí
fundingsíaún noaún no

Consulta Fuentes de datos para los detalles, incluido por qué una estrategia funding se puede registrar y sus datos preparar, pero todavía no ejecutar.

Cuando algo va mal

Los errores se reportan contra tu fichero, nunca contra el Java generado. Registrar un fuente con un error es un 400 cuyo mensaje lleva entradas Line N, Column M:, tanto para errores propios de QTScript (una sección desconocida, un periodo inválido, un parámetro duplicado, un patrón de instrumento mal formado) como para errores de Java dentro de un cuerpo.

Un 200 de POST /strategy significa que el fuente se analizó y compiló, no que vaya a funcionar. Registrar no monta la estrategia, así que lo que solo aparece al montarla lo halla validate, que lo reporta contra tu fichero de la misma forma. El caso a conocer es una ventana sobre un indicador que no está registrado, window noexiste m1 { ... }: se registra, y validate acaba failed en la línea de la ventana, QTScript line 4: unknown indicator 'noexiste' (consulta En qué indicador está una ventana). Llama a validate antes de ejecutar una estrategia que acabas de escribir.

Un fallo mientras corre la estrategia se registra en el job de la misma forma, en la línea de la que viene el cuerpo:

QTScript line 6: Index 2 out of bounds for length 1

El id de la estrategia

POST /strategy devuelve el mismo strategyId para el mismo fuente. Para QTScript el id sale del texto, porque la indentación es parte de la gramática: una marca de orden de bytes (BOM), el estilo de saltos de línea, los espacios en blanco al final de línea y las líneas en blanco antes de la primera y después de la última se ignoran; cualquier otra cosa — un comentario, la indentación, una línea en blanco intermedia — da un id distinto. (Las estrategias Java son más tolerantes; consulta Estrategias.)

Qué no hace

Por diseño, y cada punto es una razón para escribir la estrategia en Java en su lugar:

  • Sin update(). Las ventanas son el modelo; una estrategia que necesita ver cada tick pertenece a Java.
  • Sin lógica entre instrumentos — leer los indicadores de otros instrumentos necesita la clase completa.
  • Sin clases de indicador personalizadas, sin campos o métodos extra más allá de los que declaran las secciones, y sin estrategias multi-fuente.
  • Una sola clase. Cualquier cosa que quiera tipos auxiliares es una estrategia Java.

Cambiar nunca es un callejón sin salida: un fichero .qtscript es una clase Java con la ceremonia quitada.

Para ir más allá

La skill mantenida qtsurfer-qtscript-strategy tiene la referencia completa del lenguaje — cada sección, el filtro de instrumentos en detalle, nombres reservados, y más ejemplos — y funciona con agentes que leen skills:

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

Los cuerpos se escriben contra la API de estrategias documentada en la skill qtsurfer-java-strategy y en Programar estrategias en Java.