Build a MACD robot
This is the first complete robot, and the assembly chain every later strategy repeats: an account, a market adapter, an execution, a contract, an input holding the data processor, patterns over the input, rules bound to the patterns, a robot over the rules, a start, and then the replay of the history.
The assembly
Declare in dependency order. Nothing touches data until the robot is started, so the whole graph can be wired before a single tick arrives.
import tse_helpers as H
account = tse.Account("AAPL", tse.StorageRegime.Mem, lib_path=H.LIB_PATH)
market = account.create_market("MD", tse.MdType.Ohlcv)
account.create_simulator("Sim", H.simulator_options())
account.add_contract("AAPL", 1, tse.Instrument.Equity, tse.Underlying.Undefined,
tse.Venue.Undefined, 100000)
The execution here is the built-in Simulator, which fills orders from the market data as ticks arrive. It is an execution like any other, and the robot above it cannot tell the difference — which is the mechanical reason a backtested robot and a live one are the same robot rather than two implementations of one idea.
The processor
The stand-alone data processor is where the money is made, and it stays yours. Here it is a plain MACD: the distance between a twelve-day and a twenty-six-day exponential average of the close. It is positive while the recent days are stronger than the older ones, and negative when they fade.
The processor receives the input's storage handle, the contract the tick belongs to and the tick itself. It pushes the distance into the storage with the tick's timestamp, and it returns a readiness flag. That flag is the important part: the averages are meaningless until the slow one has seen enough days, so the indicator declares itself ready only then and no rule fires on a half-formed indicator.
fast, slow = 12, 26
alpha_fast, alpha_slow = 2.0 / (fast + 1), 2.0 / (slow + 1)
ema = {"fast": None, "slow": None}
def macd(storage, contract_id, tick):
ema["fast"] = tick.close if ema["fast"] is None else ema["fast"] + alpha_fast * (tick.close - ema["fast"])
ema["slow"] = tick.close if ema["slow"] is None else ema["slow"] + alpha_slow * (tick.close - ema["slow"])
storage.push(tick.tsNanoseconds, ema["fast"] - ema["slow"])
return storage.size() >= slow
The engine never looks inside this function. It holds a function pointer of a fixed signature and calls it on every tick; whether a person or a model wrote what is behind it changes nothing.
The input and the patterns
The input caches the processor's output over the contracts it covers on one adapter. Its cache length and its duration are both explicit — the engine derives neither.
account.add_input_ohlcv("MACD", slow, tse.Duration.Days, macd, market, ["AAPL"])
account.add_pattern_threshold("ToLong", tse.Duration.Days, ["MACD"], tse.Cmp.Ge, 0.0)
account.add_pattern_threshold("ToShort", tse.Duration.Days, ["MACD"], tse.Cmp.Lt, 0.0)
Two thresholds at zero are the whole decision: one holds while the momentum is positive, the other while it is negative. A pattern produces nothing tradeable by itself — it emits a signal carrying the timestamp of the input update that produced it, and a rule that names the pattern turns that signal into an order.
The rules and the robot
Buy a hundred shares once the momentum turns positive, and sell the whole position when it turns negative. Everything the order will carry is fixed in the rule's parameters when the rule is built; the firing supplies only the moment.
account.add_rule_market("Entry", tse.RuleType.Entry, H.entry_params(100.0), "ToLong", "AAPL")
account.add_rule_market("Exit", tse.RuleType.Exit, H.exit_params(), "ToShort", "AAPL")
account.add_robot("Strat", ["Entry", "Exit"])
account.start("Strat")
Adding the robot transitively takes in the whole dependency component: the rules name their patterns, the patterns name the input, and the input names its adapter and contracts.
Replay and read back
Data is pushed tick by tick, each push addressing exactly one contract. When the replay is over, the summary is read from the account.
for tick in rows:
market.push_ohlcv_by_name("AAPL", tick)
summary = account.get_summary()
print("netProfit={:.4f} trades={}".format(summary.totalNetProfit, summary.totalNumberOfTrades))
Beyond the summary sit the per-trade records the blotter journaled and the duration-bucketed ex-post scores, which are the same for this two-line indicator as for a deep model — which is what keeps candidates comparable.
The code
The complete example, in C++ and in Python: 02 hello world - macd.
Version 2.0