QTSurfer beta

Strategy examples

Use complete Java strategies that demonstrate the supported lifecycle.

1. EMA Crossover (update loop)

Buy when fast EMA crosses above slow EMA; sell on cross below.

import com.wualabs.qtsurfer.engine.core.Instrument;
import com.wualabs.qtsurfer.engine.core.Ticker;
import com.wualabs.qtsurfer.engine.indicators.helpers.group.InstrumentGroupRTIndicator;
import com.wualabs.qtsurfer.engine.strategy.AbstractTickerStrategy;

public class EmaCrossoverStrategy extends AbstractTickerStrategy {

    private Boolean fastAboveSlow;

    @Override
    protected void setupIndicators(InstrumentGroupRTIndicator indicators) {
        indicators.addPrice().ema("fast", 9).ema("slow", 21);
    }

    @Override
    public void update(Ticker ticker) {
        Instrument inst = ticker.instrument();
        updateInstrument(inst, ticker.timestamp());
        var ind = updateIndicators(inst, ticker);

        if (!ind.getExisting("slow").isReady()) return;

        boolean currentFastAbove = ind.getValue("fast") > ind.getValue("slow");
        if (fastAboveSlow == null) { fastAboveSlow = currentFastAbove; return; }

        if (currentFastAbove && !fastAboveSlow)  emitBuy(ticker.last());
        if (!currentFastAbove && fastAboveSlow)  emitSell(ticker.last());
        fastAboveSlow = currentFastAbove;
    }
}

2. RSI Oversold/Overbought (window listener)

Enter long when RSI drops below 30; exit when it rises above 70. Fires once per 1-second window, not on every tick.

import com.wualabs.qtsurfer.engine.indicators.helpers.group.InstrumentGroupRTIndicator;
import com.wualabs.qtsurfer.engine.indicators.helpers.WindowTimeRTIndicator.WindowTime;
import com.wualabs.qtsurfer.engine.strategy.AbstractWindowListener;
import com.wualabs.qtsurfer.engine.strategy.AbstractTickerStrategy;
import com.wualabs.qtsurfer.engine.core.state.StateStore;

public class RsiStrategy extends AbstractTickerStrategy {

    @Override
    protected void setupIndicators(InstrumentGroupRTIndicator indicators) {
        indicators
            .addPrice()
            .rsi(14)
            .window("rsi14", WindowTime.s1, new RsiListener(this, indicators));
    }

    private class RsiListener extends AbstractWindowListener {

        RsiListener(AbstractTickerStrategy s, InstrumentGroupRTIndicator ind) {
            super(s, ind);
        }

        @Override
        public void onChange(StateStore store, double prev, double actual) {
            if (actual < 30 && !store.is("inPosition")) {
                emitBuy(indicators.getValue("price"));
                store.set("inPosition");
            }
            if (actual > 70 && store.is("inPosition")) {
                emitSell(indicators.getValue("price"));
                store.unset("inPosition");
            }
        }
    }
}

3. Forced Trade (periodic buy/sell)

Simple stress-test strategy: buy at tick 60, sell at tick 120, repeat. Used in CI integration tests — known to compile and run correctly.

import com.wualabs.qtsurfer.engine.indicators.helpers.group.InstrumentGroupRTIndicator;
import com.wualabs.qtsurfer.engine.indicators.helpers.WindowTimeRTIndicator.WindowTime;
import com.wualabs.qtsurfer.engine.strategy.AbstractWindowListener;
import com.wualabs.qtsurfer.engine.strategy.AbstractTickerStrategy;
import com.wualabs.qtsurfer.engine.core.state.StateStore;

public class ForcedTradeStrategy extends AbstractTickerStrategy {

    @Override
    protected void setupIndicators(InstrumentGroupRTIndicator indicators) {
        indicators.addPrice().window("price", WindowTime.s1,
            new TradeListener(this, indicators));
    }

    private class TradeListener extends AbstractWindowListener {

        TradeListener(AbstractTickerStrategy s, InstrumentGroupRTIndicator ind) {
            super(s, ind);
        }

        @Override
        public void onChange(StateStore store, double prev, double actual) {
            long count = store.inc("count");
            if (count % 120 == 60) emitBuy(actual);
            else if (count % 120 == 0) emitSell(actual);
        }
    }
}

4. Bollinger Band Mean Reversion

Buy when price touches the lower band; sell at the upper band.

import com.wualabs.qtsurfer.engine.indicators.helpers.group.InstrumentGroupRTIndicator;
import com.wualabs.qtsurfer.engine.indicators.helpers.WindowTimeRTIndicator.WindowTime;
import com.wualabs.qtsurfer.engine.strategy.AbstractWindowListener;
import com.wualabs.qtsurfer.engine.strategy.AbstractTickerStrategy;
import com.wualabs.qtsurfer.engine.core.state.StateStore;

public class BollingerReversionStrategy extends AbstractTickerStrategy {

    @Override
    protected void setupIndicators(InstrumentGroupRTIndicator indicators) {
        indicators
            .addPrice()
            .bollinger("bb", 20, 2.0)   // → "bb", "bbUpper", "bbLower"
            .window("price", WindowTime.s5, new BandListener(this, indicators));
    }

    private class BandListener extends AbstractWindowListener {

        BandListener(AbstractTickerStrategy s, InstrumentGroupRTIndicator ind) {
            super(s, ind);
        }

        @Override
        public void onChange(StateStore store, double prev, double actual) {
            if (!indicators.getExisting("bb").isReady()) return;

            double upper = indicators.getValue("bbUpper");
            double lower = indicators.getValue("bbLower");

            if (actual <= lower && !store.is("long")) {
                emitBuy(actual);
                store.set("long");
                store.unset("short");
            } else if (actual >= upper && !store.is("short")) {
                emitSell(actual);
                store.set("short");
                store.unset("long");
            }
        }
    }
}

5. Configurable dual-EMA with properties

Strategy parameters configurable at submission time via @StrategyProperty.

import com.wualabs.qtsurfer.engine.indicators.helpers.group.InstrumentGroupRTIndicator;
import com.wualabs.qtsurfer.engine.strategy.AbstractWindowListener;
import com.wualabs.qtsurfer.engine.strategy.AbstractTickerStrategy;
import com.wualabs.qtsurfer.engine.strategy.CrossDetector;
import com.wualabs.qtsurfer.engine.strategy.StrategyProperty;
import com.wualabs.qtsurfer.engine.core.state.StateStore;

import java.time.Duration;

public class ConfigurableEmaStrategy extends AbstractTickerStrategy {

    @StrategyProperty(name = "ema.fast", description = "Fast EMA period", defaultValue = "9")
    private int fastPeriod = 9;

    @StrategyProperty(name = "ema.slow", description = "Slow EMA period", defaultValue = "21")
    private int slowPeriod = 21;

    @StrategyProperty(name = "window.seconds", description = "Window in seconds", defaultValue = "1")
    private int windowSeconds = 1;

    @Override
    protected void setupIndicators(InstrumentGroupRTIndicator indicators) {
        indicators
            .addPrice()
            .ema("fast", fastPeriod)
            .ema("slow", slowPeriod)
            .window("fast", Duration.ofSeconds(windowSeconds),
                new CrossListener(this, indicators));
    }

    private class CrossListener extends AbstractWindowListener {
        private final CrossDetector cross = new CrossDetector();

        CrossListener(AbstractTickerStrategy s, InstrumentGroupRTIndicator ind) {
            super(s, ind);
        }

        @Override
        public void onChange(StateStore store, double prev, double actual) {
            if (!indicators.getExisting("slow").isReady()) return;
            double slow = indicators.getValue("slow");
            CrossDetector.Cross result = cross.check(actual, slow);
            if (result.above()) emitBuy(actual);
            if (result.below()) emitSell(actual);
        }
    }
}