Plug in an ML model
This is the counterpart of the MACD robot: the surroundings are identical, only the processor changed. The robot scaffold does not change when the decision logic becomes a machine-learning model, because the engine never inspects the callable it was handed — it sees a number and a readiness flag.
Train before the run
The gradient-boosted model is trained before the run starts and then lives inside the data processor. Training is entirely on your side of the boundary; the engine has no opinion about it and no API for it.
import tse_helpers as H
model = train_model()
account = tse.Account("BTC/USDT", tse.StorageRegime.Mem, lib_path=H.LIB_PATH)
market = account.create_market("MD", tse.MdType.Bidask)
account.create_simulator("Sim", H.simulator_options())
account.add_contract("BTC", 1, tse.Instrument.Future, tse.Underlying.Crypto,
tse.Venue.Undefined, 10000)
The model inside the processor
The processor is a bid/ask processor: it derives mid price, spread and quote imbalance from every tick, asks the booster for a forecast, and pushes that forecast as the input's value. Book imbalance carries the signal the model was trained on — more volume resting on the bid than on the ask is buying pressure.
def use_gbm(model):
def processor(storage, contract_id, tick):
mid = (tick.bid + tick.ask) / 2.0
spread = tick.ask - tick.bid
denom = tick.bidVolume + tick.askVolume
imbalance = (tick.bidVolume - tick.askVolume) / denom if denom != 0.0 else 0.0
features = np.array([[mid, spread, imbalance]], dtype=np.float32)
prediction = float(model.predict(xgb.DMatrix(features))[0])
storage.push(tick.tsNanoseconds, prediction)
return storage.size() >= 1
return processor
Compare this with the MACD processor and the shape is the same: the same three arguments in,
one storage.push of a timestamped number, one readiness flag out. Whether that
number comes from a moving average, a booster, a neural network or an external cloud service is
a decision entirely on your side of the boundary, and it does not change a single line of the
robot around it.
The readiness flag is the one place the two differ, and it differs for a reason that belongs to the model rather than to the engine. The MACD had to withhold readiness until the slow average had seen enough days, because until then its value was half-formed. The booster's forecast is complete on the tick that produced it, so the input declares itself ready as soon as it holds one value. The engine reads the flag either way and does not ask why.
The wiring is unchanged
The input is a bid/ask input instead of an OHLCV one, because the feed is different. Past that, nothing about the graph reflects the fact that a model is inside it.
account.add_input_bidask("GBM", 1, tse.Duration.Minutes, use_gbm(model), market, ["BTC"])
account.add_pattern_threshold("ToLong", tse.Duration.Minutes, ["GBM"], tse.Cmp.Ge, 0.0)
account.add_pattern_threshold("ToShort", tse.Duration.Minutes, ["GBM"], tse.Cmp.Lt, 0.0)
account.add_rule_market("Entry", tse.RuleType.Entry, H.entry_params(1.0), "ToLong", "BTC")
account.add_rule_market("Exit", tse.RuleType.Exit, H.exit_params(), "ToShort", "BTC")
account.add_robot("Strat", ["Entry", "Exit"])
account.start("Strat")
Two thresholds at zero put the robot long while the forecast is positive and flat once it turns. A positive forecast means the model expects the price to rise: the robot stays long while the forecast is positive and closes the position once it turns negative.
Replay and read back
for tick in ticks:
market.push_bidask_by_name("BTC", tick)
summary = account.get_summary()
The run produces the same statistics a two-line indicator produces — the same blotter records and the same duration-bucketed ex-post scores — which is what lets a selection model rank one candidate against another.
Where the model runs
The engine runs on your own premises, and your market data, your strategy code, your models and your results never leave them. The engine reaches the network only where you point it, through the connectors you configure, and on its own behalf only for the licence check.
That is what the opaque data processor buys you. The decision-making module is separated from the order-routing pipeline, so the logic that makes the money plugs into an input as a callable the engine holds but never reads. A local booster, a neural network in your own process and a call out to an external service are the same object from where the engine stands.
The code
The complete example, in C++ and in Python: 08 robots - gbm model.
Version 2.0