QTSurfer beta

Java indicators

Configure, compose, and extend the QTSurfer real-time indicator pipeline.

All methods below are on InstrumentGroupRTIndicator and return this for chaining. Default indicator name (when name is omitted) is the method name + parameters, e.g. rsi14.

Price sources

.addPrice()                            // close price → "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

Moving averages

.sma(20)                               // 20-period SMA → "sma20"
.sma("s20", 20)                        // custom name
.sma("s20", 20, false)                 // continuous mode (default: discrete)
.sma("s20", "rsi14", 20)              // SMA of another indicator
.ema(9)                                // 9-period EMA → "ema9"
.ema("fast", 9)
.ema("fast", "vol", 9)                // EMA of volume

Oscillators & momentum

.rsi(14)                               // Cutler's RSI → "rsi14"
.rsi(14, false)                        // Wilder's smoothing
.rsi("myRsi", 14, true)

.bollinger("bb", 20, 2.0)             // → "bb", "bbUpper", "bbLower"
.bollingerBandwidth("bb")             // % width of a Bollinger band

Rate of change & distance

.percentChange("price")                // % change tick-over-tick
.rateChange("price")                   // absolute rate of change
.rateChange("rc", "price", true)      // percent=true
.distanceMa("ema9")                   // % distance from MA
.distance("gap", "ema9", "ema21")     // % distance between two indicators

Gain / loss / extremes

.gain("price")                         // consecutive gain periods
.loss("price")                         // consecutive loss periods
.gain("g", "price", false)            // resetPeriodsOnSustain=false
.max("price")                          // running max
.min("price")                          // running min
.sum("vol")                            // running sum

Arithmetic

.add("spread", "ask", "bid")          // spread = ask + bid
.diff("spread", "ask", "bid")         // diff = ask - bid
.mul("price", 0.01)                   // scale by coefficient
.mul("ratio", "vol", "price")         // vol * price
.fun("custom", "a", "b", (a, b) -> a / b)  // arbitrary BiFunction

Predicates & conditionals

.lessThan("oversold", "rsi14", 30)    // boolean: rsi14 < 30
.greatThan("overbought", "rsi14", 70)
.greatOrEqual("ge", "price", 50000)
.lessOrEqual("le", "price", 50000)
.equal("eq", "price", 100)
.notEqual("ne", "price", 100)
.predicate("custom", "price", v -> v > 0 && v < 100)
.periodCount("cnt", "oversold", v -> v > 0)  // count consecutive true periods

Conditional selection

// If indicator == coef → thenIndicator else elseIndicator
.equal("selected", "signal", 1, "emaFast", "emaSlow")
.conditional("out", "flag", ind -> ind.getValue() > 0, thenInd, elseInd)

Transformations

.clamp("price", 0.0, 100.0)          // clamp to [min, max]
.clamp("price", v -> v < 0, 0.0)     // clamp when predicate true
.round("price", 2)                    // round to N decimals
.decorate("price", "price", ind -> new MyWrapper(ind))

Window listeners

.window("ema9", WindowTime.s1, listener)     // fire every 1 s
.window("ema9", Duration.ofSeconds(15), l)   // custom duration
.window()                                     // builder pattern
    .windowTime(WindowTime.m5)
    .indicator("rsi14")
    .listener(myListener)
    .build()

Composing indicators (read-only access)

When building a custom indicator that references another, use a read-only view to avoid mutating shared state. Two equivalent approaches:

// Option A — .ro() on any RTIndicator instance (default method on RTIndicator)
RTIndicator src = indicators.getExisting("ema9").ro();
indicators.add("custom", new MyIndicator(src));

// Option B — getReadOnlyExisting() on the indicator group
RTIndicator src = indicators.getReadOnlyExisting("ema9");
indicators.add("custom", new MyIndicator(src));

// Option C — getReadOnly() returns Optional (safe if indicator may not exist)
indicators.getReadOnly("ema9").ifPresent(src ->
    indicators.add("custom", new MyIndicator(src)));

.ro() is a default method on RTIndicator itself — available on every indicator instance without going through the group. Use it when you already hold a reference to the indicator object.

Advanced indicator catalogue (statistics & pro)

Beyond the fluent builder methods above, the engine ships ~150 indicator classes across two tiers, one sub-package per category: com.wualabs.qtsurfer.engine.indicators.<category> for the free tier (averages, momentum, distance, bollinger, statistics, …) and com.wualabs.qtsurfer.engine.indicators.<category>.pro for the paid, server-private tier (averages.pro, trend.pro, momentum.pro, volatility.pro, volume.pro, statistics.pro, …) — a pro package segment is always the paid-tier marker. Pro classes are not shipped in the OSS/TypeScript port of the engine. They are plain RTIndicator instances — add any of them by class with .add("name", new XxxRTIndicator(...)), then read with indicators.getValue("name"):

import com.wualabs.qtsurfer.engine.indicators.statistics.StandardDeviationRTIndicator;
import com.wualabs.qtsurfer.engine.indicators.statistics.pro.ZScoreRTIndicator;

indicators
    .addPrice()                                                      // "price"
    .sma("mean", 20)
    .add("std",    new StandardDeviationRTIndicator(20))             // ctor (int periods)
    .add("stdOf",  new StandardDeviationRTIndicator(                 // ctor (RTIndicator, int periods)
            indicators.getReadOnlyExisting("mean"), 20))
    .add("zscore", new ZScoreRTIndicator(/* see class for ctor */));

Constructors vary per class — most take (int periods) and/or (RTIndicator source, int periods); some (lombok-built) differ, so check the class. Useful classes by category — Trend, Volume, and performance Ratios are pro-only today, the free tier has no indicator in those categories yet:

CategoryTier*RTIndicator classes
Moving averagesFreeSma, Ema, Wma, Hma, Kama, Tema, Mma
Moving averagesProAlma, Dema, Frama, LeastSquaresMovingAverage, McGinleyDynamic, Smma, Wma (O(1) twin), Envelopes
StatisticsFreeStandardDeviation, Variance
StatisticsProStandardDeviation, Variance (O(1) twins), ZScore, Skewness, Kurtosis, RollingPercentile, Correlation, Covariance, Beta, LinearRegressionSlope, SimpleLinearRegression
TrendPro onlyAdx, Aroon, SuperTrend, ParabolicSar, Ichimoku, DonchianChannel, EfficiencyRatio
VolatilityFreeVolatilityRTIndicator, PercentVolatilityRTIndicator
VolatilityProAtr, Natr, RealizedVolatility, Parkinson, GarmanKlass, EwmaVolatility
VolumePro onlyVwap, Obv, Mfi, Cmf, Adl, ElderForceIndex
OscillatorsFreeMacd, StochasticRsi, Cci
OscillatorsProStochasticOscillator, Roc, Momentum, WilliamsR, UltimateOscillator
Ratios (performance)Pro onlySharpeRatio, SortinoRatio, CalmarRatio, MaxDrawdown, OmegaRatio, UlcerIndex

Compose them by feeding one indicator’s read-only view into another’s (RTIndicator, …) constructor (e.g. a ZScore of an Sma). This is how to do rolling stats / aggregation without reinventing the wheel in update().

Writing a custom RTIndicator

When no built-in fits, implement the RTIndicator interface (com.wualabs.qtsurfer.engine.indicators.core.RTIndicator) — or extend AbstractRTIndicator for the common scaffolding:

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

public class MyIndicator implements RTIndicator {
    private double value;
    private boolean ready;

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

    @Override public double update(double newValue) {     // called once per tick with the source value
        this.value = /* compute incrementally from newValue */ newValue;
        this.ready = true;
        return value;
    }

    @Override public boolean isReady() { return ready; }  // gate warmup (default true)

    @Override public void reset() { value = 0; ready = false; }  // from Resettable
}

Register it like any built-in: indicators.add("myInd", new MyIndicator()). The interface is small: getValue() (current output), update(double) (incremental, per tick), isReady() (warmup gate, default true), reset(). update(Number) / update(RTIndicator) and ro() (read-only view) come as default methods for free.

Rich indicators (full snapshot access)

Scalar indicators receive a single double (one field, extracted via TickerValueSource). When an indicator needs more than one field — OHLC for ATR, volume+price for VWAP/OBV, bid/ask for microstructure — implement RichRTIndicator<T extends MarketSnapshot> instead and read the whole snapshot:

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

public class MyOhlcIndicator implements RichRTIndicator<MarketSnapshot> {
    private double value;
    @Override public double updateFrom(MarketSnapshot snap) {  // full snapshot: O/H/L/C/V, bid/ask
        this.value = /* combine several fields */ 0;
        return value;
    }
    @Override public double getValue() { return value; }
    @Override public double update(double v) { return value; }  // scalar path unused
    @Override public void reset() { value = 0; }
}

The engine builds the snapshot once per tick and dispatches it to every registered RichRTIndicator, while scalar indicators keep receiving their extracted field. Register it the same way: indicators.add("myOhlc", new MyOhlcIndicator()).

Hidden indicators

Prefix with _ to exclude from signal reporting metadata:

.gain("_rawGain", "price")   // internal use, not reported

This is sugar over the VISIBILITY metadata entry — see below.

Indicator metadata

Every indicator carries small descriptive key/value metadata about itself, kept separate from its registered lookup name, readable from any RTIndicator instance (e.g. via indicators.getExisting("name") / getReadOnlyExisting("name")):

RTIndicator ind = indicators.getExisting("gap");
ind.getId();                     // canonical type id, e.g. "distance", "bollinger", "rsi"
ind.getDisplayHint();            // DisplayHint: ABSOLUTE (default), PERCENT, or VOLUME
ind.isHidden();                  // true if internal-only (the "_" prefix above sets this)
ind.getMeta().get("periods");    // any other descriptive key, or null if unset

Useful for introspection without parsing the name string — e.g. checking getDisplayHint() == DisplayHint.PERCENT before formatting a value for display, or getId() to branch generically over whatever indicator is registered under a name. distance() / percentChange() / distanceMa() set PERCENT automatically; most other indicators carry no metadata — getMeta() returns the shared IndicatorMeta.EMPTY, never null.

To attach metadata on a custom indicator (see Writing a custom RTIndicator), extend AbstractRTIndicator and use its fluent setters at registration:

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 MyDistanceIndicator(a, b)
        .withMeta(IndicatorMeta.ID, "distance")
        .withMeta("periods", 20)
        .withDisplayHint(DisplayHint.PERCENT));

Metadata is a write-only descriptor — an indicator must never read its own metadata back to drive its computation, that would make it a second, undeclared configuration channel. Set it once at registration; read it only from the outside.