Indicadores en Java

Configura, compón y amplía el pipeline de indicadores en tiempo real de QTSurfer.

Todos los métodos de abajo están en InstrumentGroupRTIndicator y devuelven this para encadenar. El nombre por defecto de un indicador (cuando se omite name) es el nombre del método más los parámetros, por ejemplo rsi14.

Fuentes de precio

.addPrice()                            // precio de cierre → "price"
.add("bid",  TickerValueSource.Bid)
.add("ask",  TickerValueSource.Ask)
.add("vol",  TickerValueSource.Volume)
// TickerValueSource: Bid, BidSize, Ask, AskSize, Open, High, Low, Close,
//                   Vwap, Volume, VolumeQuote, PercentChange, AutoAskClose

Medias móviles

.sma(20)                               // SMA de 20 periodos → "sma20"
.sma("s20", 20)                        // nombre personalizado
.sma("s20", 20, false)                 // modo continuo (por defecto: discreto)
.sma("s20", "rsi14", 20)              // SMA de otro indicador
.ema(9)                                // EMA de 9 periodos → "ema9"
.ema("rapida", 9)
.ema("rapida", "vol", 9)              // EMA del volumen

Medias, estadística y volatilidad

Gratuitos, en el mismo builder. Llamados sin un nombre propio, registran el nombre que indica el comentario.

.wma(5)                                // media móvil ponderada → "wma5"
.tema(4)                               // EMA triple → "tema4"
.hma(6)                                // media móvil de Hull → "hma6"
.mma(7)                                // → "mma7"
.kama()                                // adaptativa de Kaufman → "kama10_2_30"
.stdDev(5)                             // desviación típica → "stdDev5"
.variance(5)                           // → "variance5"
.volatility(5)                         // → "vol5"
.percentVolatility(5)                  // → "pvol5"
.bollingerPercentB()                   // %B de una banda de Bollinger → "pctB20_2"
.atr(14)                               // rango verdadero medio → "atr14"

atr lee barras completas, igual que los indicadores pro: úsalo en una estrategia kline. En una estrategia ticker sus valores no son los de la barra y no significan nada. wma y atr son métodos del builder, gratuitos en todos los planes; ninguno de los dos tiene llamada en Pro.

Osciladores y momento

.rsi(14)                               // RSI de Cutler → "rsi14"
.rsi(14, false)                        // suavizado de Wilder
.rsi("miRsi", 14, true)

.cci(20)                               // → "cci20"
.macd()                                // MACD 12/26 de la fuente del grupo → "macd"
.macd("m", 8, 21)                      // periodos de EMA a medida
.stochRsi(14)                          // RSI estocástico, salida de 0 a 100 → "stochRsi14"

.bollinger("bb", 20, 2.0)             // → "bb", "bbUpper", "bbLower"
.bollingerBandwidth("bb")             // % de anchura de una banda de Bollinger

Tasa de cambio y distancia

.percentChange("price")                // % de cambio tick a tick
.rateChange("price")                   // tasa de cambio absoluta
.rateChange("rc", "price", true)      // percent=true
.distanceMa("ema9")                   // % de distancia respecto a la MA
.distance("gap", "ema9", "ema21")     // % de distancia entre dos indicadores

Ganancia / pérdida / extremos

.gain("price")                         // periodos consecutivos de ganancia
.loss("price")                         // periodos consecutivos de pérdida
.gain("g", "price", false)            // resetPeriodsOnSustain=false
.max("price")                          // máximo acumulado
.min("price")                          // mínimo acumulado
.sum("vol")                            // suma acumulada

Aritmética

.add("spread", "ask", "bid")          // spread = ask + bid
.diff("spread", "ask", "bid")         // diff = ask - bid
.mul("price", 0.01)                   // escala por un coeficiente
.mul("ratio", "vol", "price")         // vol * price
.fun("personalizada", "a", "b", (a, b) -> a / b)  // BiFunction arbitraria

Predicados y condicionales

.lessThan("sobrevendido", "rsi14", 30)    // booleano: rsi14 < 30
.greatThan("sobrecomprado", "rsi14", 70)
.greatOrEqual("ge", "price", 50000)
.lessOrEqual("le", "price", 50000)
.equal("eq", "price", 100)
.notEqual("ne", "price", 100)
.predicate("personalizada", "price", v -> v > 0 && v < 100)
.periodCount("cnt", "sobrevendido", v -> v > 0)  // cuenta periodos consecutivos en true

Selección condicional

// Si indicador == coef → entoncesInd si no sinoInd
.equal("seleccionado", "senal", 1, "emaRapida", "emaLenta")
.conditional("salida", "bandera", ind -> ind.getValue() > 0, entoncesInd, sinoInd)

Transformaciones

.clamp("price", 0.0, 100.0)          // limita el valor a [min, max]
.clamp("price", v -> v < 0, 0.0)     // aplica el clamp cuando el predicado es true
.round("price", 2)                    // redondea a N decimales
.decorate("price", "price", ind -> new MiEnvoltorio(ind))

Window listeners

.window("ema9", WindowTime.s1, listener)     // se dispara cada 1 s
.window("ema9", Duration.ofSeconds(15), l)   // duración personalizada
.window()                                     // patrón builder
    .windowTime(WindowTime.m5)
    .indicator("rsi14")
    .listener(myListener)
    .build()

Composición de indicadores (acceso de solo lectura)

Al construir un indicador personalizado que referencia a otro, usa una vista de solo lectura para evitar mutar el estado compartido. Dos enfoques equivalentes:

// Opción A — .ro() en cualquier instancia de RTIndicator (método por defecto de RTIndicator)
RTIndicator fuente = indicators.getExisting("ema9").ro();
indicators.add("personalizado", new MiIndicador(fuente));

// Opción B — getReadOnlyExisting() en el grupo de indicadores
RTIndicator fuente = indicators.getReadOnlyExisting("ema9");
indicators.add("personalizado", new MiIndicador(fuente));

// Opción C — getReadOnly() devuelve Optional (seguro si el indicador puede no existir)
indicators.getReadOnly("ema9").ifPresent(fuente ->
    indicators.add("personalizado", new MiIndicador(fuente)));

.ro() es un método por defecto del propio RTIndicator — disponible en cualquier instancia de indicador sin pasar por el grupo. Úsalo cuando ya tienes una referencia al objeto indicador.

Los indicadores pro

Los indicadores pro vienen con los planes Pro y Elite. En Java son los métodos de Pro, en com.wualabs.qtsurfer.engine.indicators.pro: Pro.on(indicators) recibe el grupo de indicadores que recibe setupIndicators, y cada llamada registra un indicador pro con los mismos nombres, formas e ids que lista la tabla bajo “The pro indicators: uses” de la skill qtsurfer-qtscript-strategy (adx, cmf, garmanKlass, ichimoku, kurtosis, mfi, obv, parkinson, rogersSatchell, superTrend, vwap, yangZhang). Pro.on(indicators).adx(14) registra adx14 y, a su lado, adx14PlusDI y adx14MinusDI. Una ventana va sobre el principal (adx14), y el listener lee los demás por nombre.

Leen barras enteras, así que úsalos en una estrategia kline. Esta compra cuando el ADX dice que hay tendencia y la línea plus está por encima de la minus, y vende cuando está por debajo:

import com.wualabs.qtsurfer.engine.core.KlineInterval;
import com.wualabs.qtsurfer.engine.core.state.StateStore;
import com.wualabs.qtsurfer.engine.indicators.core.KlineValueSource;
import com.wualabs.qtsurfer.engine.indicators.helpers.WindowTimeRTIndicator.WindowTime;
import com.wualabs.qtsurfer.engine.indicators.helpers.group.InstrumentGroupRTIndicator;
import com.wualabs.qtsurfer.engine.indicators.pro.Pro;
import com.wualabs.qtsurfer.engine.strategy.AbstractKlineStrategy;
import com.wualabs.qtsurfer.engine.strategy.AbstractWindowListener;

public class AdxTrend extends AbstractKlineStrategy {

  @Override
  public KlineInterval getInterval() {
    return KlineInterval.s1;
  }

  @Override
  protected void setupIndicators(InstrumentGroupRTIndicator indicators) {
    indicators.addPrice();
    indicators.add("open", KlineValueSource.Open);
    indicators.add("high", KlineValueSource.High);
    indicators.add("low", KlineValueSource.Low);
    indicators.add("close", KlineValueSource.Close);
    indicators.add("volume", KlineValueSource.Volume);
    Pro.on(indicators).adx(14);
    indicators.window("adx14", WindowTime.m1, new TrendListener(this, indicators));
  }

  private class TrendListener extends AbstractWindowListener {

    TrendListener(AbstractKlineStrategy strategy, InstrumentGroupRTIndicator indicators) {
      super(strategy, indicators);
    }

    @Override
    public void onChange(StateStore store, double prev, double actual) {
      double close = indicators.getValue("close");
      double plus = indicators.getValue("adx14PlusDI");
      double minus = indicators.getValue("adx14MinusDI");
      if (actual > 20 && plus > minus) emitBuy(close);
      if (actual > 20 && plus < minus) emitSell(close);
    }
  }
}

En una estrategia ticker, un indicador pro no se rechaza, pero sus valores no son los de la barra y no significan nada, y validar o ejecutar la estrategia devuelve un aviso del motor que lo dice.

Registrar una estrategia que los usa se acepta en cualquier plan, y la respuesta de POST /strategy lleva requires: ["indicators.pro"]. Ejecutarla en un plan que no los incluye se rechaza con 402, al validarla, al lanzar un backtest o un barrido con ella y al lanzar una ejecución en vivo con ella. El mensaje nombra la primera llamada que hace la estrategia y la línea de tu fuente, aquí strategy uses pro indicators (Pro.adx, line 25); your plan: free. La comprobación lee la estrategia compilada, así que cualquier otra mención de una clase del paquete pro cuenta igual, y entonces el mensaje nombra la clase. El estado, el cuerpo y el upgradeUrl son los que describe “The pro indicators and your plan” en la skill qtsurfer-qtscript-strategy.

Catálogo de indicadores avanzado (estadística y pro)

Más allá de los métodos del builder fluido de arriba, el motor incluye unas 150 clases de indicadores en dos niveles, un subpaquete por categoría: com.wualabs.qtsurfer.engine.indicators.<category> para el nivel gratuito (averages, momentum, distance, bollinger, statistics, …) y com.wualabs.qtsurfer.engine.indicators.<category>.pro para el nivel pro (averages.pro, trend.pro, momentum.pro, volatility.pro, volume.pro, statistics.pro, …) — el segmento de paquete pro es siempre la marca del nivel pro. Las clases pro vienen con los planes Pro y Elite, y no se distribuyen en el port OSS/TypeScript del motor. Son instancias RTIndicator normales — añade cualquiera de ellas por clase con .add("name", new XxxRTIndicator(...)), y léelas con indicators.getValue("name").

Una estrategia enviada puede usar una clase pro. import com.wualabs.qtsurfer.engine.indicators.statistics.pro.ZScoreRTIndicator; compila, y la estrategia se registra en cualquier plan; lo que un plan sin los indicadores pro no puede hacer es ejecutarla (consulta «Los indicadores pro» más arriba). La comprobación lee la estrategia compilada, así que cualquier mención de una clase pro cuenta: un import que el código usa, un cast, un campo, una firma. El ejemplo de abajo muestra la forma de la llamada; su última línea es pro, las demás son gratuitas.

import com.wualabs.qtsurfer.engine.indicators.statistics.StandardDeviationRTIndicator;
import com.wualabs.qtsurfer.engine.indicators.statistics.pro.ZScoreRTIndicator;   // pro: necesita el plan Pro o Elite para ejecutarse

indicators
    .addPrice()                                                      // "price"
    .sma("media", 20)
    .add("desv",    new StandardDeviationRTIndicator(20))             // constructor (int periods)
    .add("desvDe",  new StandardDeviationRTIndicator(                 // constructor (RTIndicator, int periods)
            indicators.getReadOnlyExisting("media"), 20))
    .add("zscore", new ZScoreRTIndicator(/* ver la clase para el constructor */));   // pro: necesita el plan Pro o Elite para ejecutarse

Los constructores varían según la clase — la mayoría acepta (int periods) y/o (RTIndicator source, int periods); algunos (construidos con lombok) difieren, así que consulta la clase. Las clases por categoría, con el paquete en el que vive cada una (bajo com.wualabs.qtsurfer.engine.indicators) — Tendencia, Volumen y los ratios de rendimiento son solo pro hoy, el nivel gratuito todavía no tiene ningún indicador en esas categorías. El nombre de una clase es el de la tabla más RTIndicator (Sma es SmaRTIndicator); el mismo nombre corto puede existir en un paquete gratuito y en uno pro (StandardDeviation, Variance), así que importa el del paquete que quieras:

CategoríaNivelPaqueteClases *RTIndicator
Medias móvilesGratuitoaveragesSma, Ema, Wma, WmaIncremental, Hma, Kama, Tema, Mma
Medias móvilesProaverages.proAlma, Dema, Envelopes, Frama, LeastSquaresMovingAverage, McGinleyDynamic, Smma
BandasGratuitobollingerBollingerBandWidth, BollingerBands, BollingerPercentB
OsciladoresGratuitomomentumRsi, Macd, StochasticRsi, Cci
OsciladoresPromomentum.proAcceleratorOscillator, AdaptiveFisherTransform, AwesomeOscillator, BalanceOfPower, BullBearPower, ChandeForecastOscillator, Cmo, ConnorsRsi, CoppockCurve, DetrendedPriceOscillator, FisherTransform, KnowSureThing, Momentum, RelativeVigorIndex, Roc, StochasticOscillator, Trix, TrueStrengthIndex, UltimateOscillator, WilliamsR
EstadísticaGratuitostatisticsStandardDeviation, Variance
EstadísticaProstatistics.proAutocorrelation, Beta, Correlation, Covariance, HurstExponent, Kurtosis, LinearRegressionSlope, RollingPercentile, SimpleLinearRegression, Skewness, StandardDeviation, ValueAtRisk, Variance, ZScore
VolatilidadGratuitovolatilityAtr
VolatilidadGratuitohelpersVolatility, PercentVolatility
VolatilidadProvolatility.proChoppinessIndex, KeltnerChannel, MassIndex, Natr, RelativeVolatilityIndex
Volatilidad (estimadores)Prostatistics.pro.volatilityEwmaVolatility, GarmanKlassVolatility, ParkinsonVolatility, RealizedVolatility, RogersSatchellVolatility, YangZhangVolatility
TendenciaSolo protrend.proAdx, Aroon, ChandeKrollStop, ChandelierExit, DonchianChannel, EfficiencyRatio, Ichimoku, ParabolicSar, SuperTrend, Vortex
VolumenSolo provolume.proAdl, ChaikinOscillator, Cmf, EaseOfMovement, ElderForceIndex, KlingerOscillator, Mfi, NegativeVolumeIndex, Obv, Pvt, Vwap
Ratios (rendimiento)Solo prostatistics.pro.ratiosCalmarRatio, InformationRatio, MaxDrawdown, OmegaRatio, SharpeRatio, SortinoRatio, TailRatio, TreynorRatio, UlcerIndex
Distancia y cambioGratuitodistanceDistance, DistanceFromMa
Distancia y cambioGratuitohelpersRateChange, PercentChangePeriod
Conteo y extremosGratuitohelpersGain, Loss, Max, Min, Count
Conteo y extremosGratuitonumericSum
Derivados (los registra el builder)GratuitonumericBiFunction, Clamp, ClampPredicate, Conditional, Predicate, Round
Derivados (los registra el builder)GratuitohelpersPredicatePeriodCounter

Compón estas clases alimentando la vista de solo lectura de un indicador al constructor (RTIndicator, …) de otro (por ejemplo, un ZScore de una Sma). Así se hacen estadísticas móviles o agregaciones sin reinventar la rueda en update().

Escribir un RTIndicator personalizado

Cuando ningún indicador integrado encaja, implementa la interfaz RTIndicator (com.wualabs.qtsurfer.engine.indicators.core.RTIndicator) — o extiende AbstractRTIndicator para el andamiaje común:

import com.wualabs.qtsurfer.engine.indicators.core.RTIndicator;

public class MiIndicador implements RTIndicator {
    private double valor;
    private boolean listo;

    @Override public double getValue() { return valor; }

    @Override public double update(double nuevoValor) {     // se llama una vez por tick con el valor de origen
        this.valor = /* calcula de forma incremental a partir de nuevoValor */ nuevoValor;
        this.listo = true;
        return valor;
    }

    @Override public boolean isReady() { return listo; }  // controla el calentamiento (por defecto true)

    @Override public void reset() { valor = 0; listo = false; }  // de Resettable
}

Regístralo como cualquier indicador integrado: indicators.add("miInd", new MiIndicador()). La interfaz es pequeña: getValue() (salida actual), update(double) (incremental, por tick), isReady() (puerta de calentamiento, por defecto true), reset(). update(Number) / update(RTIndicator) y ro() (vista de solo lectura) vienen como métodos por defecto, gratis.

Indicadores enriquecidos (acceso al snapshot completo)

Los indicadores escalares reciben un único double (un campo, extraído vía TickerValueSource). Cuando un indicador necesita más de un campo — OHLC para el ATR, volumen+precio para VWAP/OBV, bid/ask para microestructura — implementa RichRTIndicator<T extends MarketSnapshot> en su lugar y lee el snapshot completo:

import com.wualabs.qtsurfer.engine.indicators.core.RichRTIndicator;
import com.wualabs.qtsurfer.engine.core.MarketSnapshot;

public class MiIndicadorOhlc implements RichRTIndicator<MarketSnapshot> {
    private double valor;
    @Override public double updateFrom(MarketSnapshot instantanea) {  // instantánea completa: O/H/L/C/V, bid/ask
        this.valor = /* combina varios campos */ 0;
        return valor;
    }
    @Override public double getValue() { return valor; }
    @Override public double update(double v) { return valor; }  // ruta escalar sin usar
    @Override public void reset() { valor = 0; }
}

El motor construye el snapshot una vez por tick y lo despacha a cada RichRTIndicator registrado, mientras los indicadores escalares siguen recibiendo su campo extraído. Se registra igual: indicators.add("miOhlc", new MiIndicadorOhlc()).

Indicadores ocultos

Prefíjalos con _ para excluirlos de los metadatos de reporte de señales:

.gain("_gananciaCruda", "price")   // uso interno, no se reporta

Es azúcar sintáctico sobre la entrada de metadatos VISIBILITY — ver más abajo.

Metadatos de indicador

Todo indicador lleva pequeños metadatos descriptivos clave/valor sobre sí mismo, separados de su nombre de búsqueda registrado, legibles desde cualquier instancia de RTIndicator (por ejemplo, vía indicators.getExisting("name") / getReadOnlyExisting("name")):

RTIndicator ind = indicators.getExisting("gap");
ind.getId();                     // id de tipo canónico, p. ej. "distance", "bollinger", "rsi"
ind.getDisplayHint();            // DisplayHint: ABSOLUTE (por defecto), PERCENT o VOLUME
ind.isHidden();                  // true si es solo interno (el prefijo "_" de arriba lo activa)
ind.getMeta().get("periods");    // cualquier otra clave descriptiva, o null si no está definida

Útil para introspección sin analizar el nombre como cadena de texto — por ejemplo, comprobar getDisplayHint() == DisplayHint.PERCENT antes de formatear un valor para mostrarlo, o getId() para bifurcar de forma genérica según qué indicador esté registrado bajo un nombre. distance() / percentChange() / distanceMa() fijan PERCENT automáticamente; la mayoría del resto de indicadores no lleva metadatos — getMeta() devuelve el IndicatorMeta.EMPTY compartido, nunca null.

Para adjuntar metadatos a un indicador personalizado (consulta Escribir un RTIndicator personalizado), extiende AbstractRTIndicator y usa sus setters fluidos en el registro:

import com.wualabs.qtsurfer.engine.indicators.core.AbstractRTIndicator;
import com.wualabs.qtsurfer.engine.indicators.core.IndicatorMeta;
import com.wualabs.qtsurfer.engine.indicators.core.DisplayHint;

indicators.add("gap",
    new MiIndicadorDistancia(a, b)
        .withMeta(IndicatorMeta.ID, "distance")
        .withMeta("periods", 20)
        .withDisplayHint(DisplayHint.PERCENT));

Los metadatos son un descriptor de solo escritura — un indicador nunca debe leer sus propios metadatos para dirigir su cálculo, eso lo convertiría en un segundo canal de configuración no declarado. Fíjalos una sola vez al registrar; léelos solo desde fuera.