5. Pattern
What a pattern is
A pattern is the decision node of a robot: it observes one or more inputs, referenced by their labels, and emits a trading signal whenever its condition holds. Patterns produce nothing tradeable on their own — a signal becomes an order only through a rule that names the pattern (tse_add_rule_market and the other rule builders take a patternLabel, see the Rule chapter). Every signal, from every one of the six kinds, carries the timestamp of the input update that produced it and an order: a quantity, a price and a transaction side. The five analytic kinds leave that order zeroed with an undefined side, which says the pattern makes no proposal of its own; a formula processor may fill any of the three fields. A rule reads from the order only those of the three it was itself declared without (see "The signal order" below).
Every pattern builder shares the same leading parameters and the same conventions:
account— the account handle the pattern is created on; every builder returnsTseStatus(tse_ok/tse_error).label— the pattern's unique name; rules bind to it.duration— aTseDurationvalue, explicit and mandatory. The engine does not derive the duration from the observed inputs; you state it, exactly as you do for inputs.inputLabels,inputCount— the observed inputs as an array of C strings plus a count. Each kind demands an exact count and rejects any other.coreId— mandatory. A non-negative value pins the pattern's worker thread to that core; a negative value runs the pattern without a separate thread. The C++ and Python wrappers default this argument to-1.
The six kinds
tse_pattern.h declares six pattern kinds: comparison, crossover, threshold, peak, timestamp, and formula. The first five are closed analytic conditions parameterized by a TseCmp comparator and scalar arguments; the sixth hands the decision to your own code. TseCmp has six values — tse_cmp_ge, tse_cmp_lt, tse_cmp_gt, tse_cmp_le, tse_cmp_eq, tse_cmp_ne — mirrored as tse::Cmp::ge … tse::Cmp::ne in C++ and tse.Cmp.Ge … tse.Cmp.Ne in Python.
Comparison
Observes exactly two inputs and fires when lhs <cmp> rhs holds for the two input values at the same timestamp. The first label in inputLabels is the left-hand side, the second is the right-hand side.
TseStatus tse_add_pattern_comparison(TseAccountHandle account, char const* label, TseDuration duration, char const* const* inputLabels, size_t inputCount, TseCmp comparison, int coreId);
void Account::addPatternComparison(std::string label, Duration duration, std::vector<std::string> inputLabels, Cmp comparison, int coreId = -1) &;
Account.add_pattern_comparison(label, duration, input_labels, comparison, core_id=-1)
Crossover
Observes exactly two inputs and fires on the bar where lhs crosses rhs: the comparison was false on the previous pair of values and becomes true on the current pair. It therefore needs two data points per input before it can fire at all.
TseStatus tse_add_pattern_crossover(TseAccountHandle account, char const* label, TseDuration duration, char const* const* inputLabels, size_t inputCount, TseCmp comparison, int coreId);
void Account::addPatternCrossover(std::string label, Duration duration, std::vector<std::string> inputLabels, Cmp comparison, int coreId = -1) &;
Account.add_pattern_crossover(label, duration, input_labels, comparison, core_id=-1)
Threshold
Observes exactly one input and fires when value <cmp> threshold holds for the input's current value.
TseStatus tse_add_pattern_threshold(TseAccountHandle account, char const* label, TseDuration duration, char const* const* inputLabels, size_t inputCount, TseCmp comparison, double threshold, int coreId);
void Account::addPatternThreshold(std::string label, Duration duration, std::vector<std::string> inputLabels, Cmp comparison, double threshold, int coreId = -1) &;
Account.add_pattern_threshold(label, duration, input_labels, comparison, threshold, core_id=-1)
Peak
Observes exactly one input and fires when the input forms a local extremum: over the last three values, with strictly increasing timestamps, the middle value is either strictly above both neighbours (a crest) or strictly below both (a trough). Both directions fire; there is no separate "valley" kind.
TseStatus tse_add_pattern_peak(TseAccountHandle account, char const* label, TseDuration duration, char const* const* inputLabels, size_t inputCount, int coreId);
void Account::addPatternPeak(std::string label, Duration duration, std::vector<std::string> inputLabels, int coreId = -1) &;
Account.add_pattern_peak(label, duration, input_labels, core_id=-1)
Timestamp
Observes exactly one input and fires on the clock rather than on values: once the input's timestamp reaches checkPoint, the pattern fires and re-arms itself one coolDown period ahead (aligned to the cool-down grid), so it fires again every coolDown thereafter. Both arguments are epoch-nanoseconds.
TseStatus tse_add_pattern_timestamp(TseAccountHandle account, char const* label, TseDuration duration, char const* const* inputLabels, size_t inputCount, int64_t checkPointNanoseconds, int64_t coolDownNanoseconds, int coreId);
void Account::addPatternTimestamp(std::string label, Duration duration, std::vector<std::string> inputLabels, std::int64_t checkPointNanoseconds, std::int64_t coolDownNanoseconds, int coreId = -1) &;
Account.add_pattern_timestamp(label, duration, input_labels, check_point_ns, cool_down_ns, core_id=-1)
Formula
Delegates the firing decision to a user callback and accepts any non-zero number of inputs. The callback contract is the subject of the next section.
TseStatus tse_add_pattern_formula(TseAccountHandle account, char const* label, TseDuration duration, char const* const* inputLabels, size_t inputCount, TseFormulaProcessor processor, void* userData, int coreId);
void Account::addPatternFormula(std::string label, Duration duration, std::vector<std::string> inputLabels, FormulaProcessor processor, int coreId = -1) &;
Account.add_pattern_formula(label, duration, input_labels, processor, core_id=-1)
The formula escape hatch
The formula processor is invoked once per input update and decides whether the pattern fires.
The processor is handed the order the signal will carry, zeroed before every call, and whatever it writes there travels with the firing. That is what makes the formula kind the escape hatch for sizing and for direction as well as for logic: the quantity, the price and the transaction side of the resulting trade can all be computed per fire — provided the rule bound to the pattern left them undefined when it was built. Nothing written into the order survives a call: a processor that does not fire, or that reports an error, or that throws, leaves no trace in the next call's order, and the next call starts from zeroes again.
The callback's parameters and its return convention:
| Parameter | Type | Meaning |
|---|---|---|
inputLabel | char const* | Names the input whose update triggered the call — this is how a multi-input formula discriminates its fan-in: observe several inputs, keep whatever state you need in userData, and branch on the label. |
tsNanoseconds | int64_t | The update timestamp in epoch-nanoseconds. |
value | double | The input's value at that update. |
order | TseSignalOrder* | The order the signal will carry, zeroed before every call. Fill the fields the bound rule left undefined; the engine reads it only when the call fires. The pointer is valid for the duration of the call and no longer. |
errorBuffer | char* | To report an error, write a NUL-terminated message into it; a non-empty buffer suppresses the firing and the engine logs the message. Leave the buffer untouched for no error. |
errorBufferLength | size_t | The capacity of errorBuffer. |
userData | void* | The opaque pointer handed to the builder, where the formula keeps its state. |
| return value | int | Return non-zero to fire the pattern, zero to stay silent. |
The order itself is a four-field POD declared in the same header:
| Field | Type |
|---|---|
| clientOrderId | uint64_t |
| price | double |
| quantity | double |
| txnSide | TseSide |
clientOrderId is carried for your own bookkeeping and the engine does not read it: the trade a rule issues gets a fresh client order id of its own on every firing.
Its C typedef, declared in tse_pattern.h:
typedef int (*TseFormulaProcessor)(char const* inputLabel, int64_t tsNanoseconds, double value, TseSignalOrder* order, char* errorBuffer, size_t errorBufferLength, void* userData);
The C++ wrapper form drops the buffer and the userData pointer — capture state in the closure instead — and takes the order by reference; a thrown exception is treated as "do not fire":
using FormulaProcessor = std::function<bool(std::string const& inputLabel, std::int64_t tsNanoseconds, double value, SignalOrder& order)>;
The Python form is any callable with the same four parameters returning a truthy value to fire; the order arrives as a tse.TseSignalOrder whose fields are assigned in place, and an exception raised inside the callable is swallowed and treated as "do not fire":
def processor(input_label, ts_nanoseconds, value, order): ...
The signal order
A rule names the quantity, the price form and the transaction side of its trade when it is built, and it may leave any of those three undefined instead. Undefined means "the signal brings it": on every firing the rule copies that field out of the order the pattern published, and leaves the others exactly as they were declared. A rule that declared all three never looks at the order at all, and a risk rule never does either — it is rejected at build time if any of the three is undefined, because a stop has to know its own size in advance.
Two of the three fields are read through a value, not through a flag, and the reading is worth stating exactly.
- Quantity. A non-zero quantity from the signal is the size of the trade. A zero names no size at all, so the rule issues nothing and logs the reason. The whole-position form is a declaration, not something a signal can ask for.
- Price. A zero price from the signal means at market; any other value means a limit order at that price. Zero can never be a real limit, which is what makes the test safe, and it is also how the engine already stores a market price internally.
- Transaction side. Taken as the signal left it. A side the signal never set is no side, and the rule issues nothing.
The five analytic kinds publish a zeroed order, so a rule behind one of them gets a market price for the asking and nothing else: leave the price undefined behind a threshold and the trade goes out at market, but leave the quantity undefined there and the rule has nothing to issue. Quantity and side from a signal are the formula kind's business.
tse_enums.h defines TseQuantityMode with three values and TsePriceType with four; tse_quantity_mode_to_string renders a quantity mode as text. The former fourth quantity mode, tse_quantity_from_signal (numeric 3), is gone: its meaning now belongs to tse_quantity_undefined, and a rule handed the old numeric 3 is refused with a diagnostic.
| Mode | C | C++ | Python |
|---|---|---|---|
| from the signal | tse_quantity_undefined | QuantityMode::undefined | Quantity.Undefined |
| whole position | tse_quantity_all | QuantityMode::all | Quantity.All |
| fixed size | tse_quantity_fixed | QuantityMode::fixed | Quantity.Fixed |
| Price form | C | C++ | Python |
|---|---|---|---|
| from the signal | tse_price_undefined | PriceType::undefined | Price.Undefined |
| at market | tse_price_market | PriceType::market | Price.Market |
| at a limit | tse_price_limit | PriceType::limit | Price.Limit |
| general (rejected by rules) | tse_price_general | PriceType::general | Price.General |
Persistence
A robot is saved as a recipe (schema version 4, see the environment chapter), and the five analytic kinds are fully described by their builder parameters, so they round-trip through tse_save / tse_load with no extra work. A formula processor, however, is a function pointer that cannot be serialized. To keep a robot with formula patterns savable, register the processor under a string key and build the pattern by key; the recipe then stores the key, and a loading process must register the same key before tse_load.
The by-key builder takes no coreId; a pattern built by key runs without a pinned worker thread. The plain tse_add_pattern_formula still runs normally, but a robot containing such a pattern is unsavable: tse_save fails.
TseStatus tse_register_formula_processor(TseAccountHandle account, char const* key, TseFormulaProcessor processor, void* userData);
TseStatus tse_add_pattern_formula_by_key(TseAccountHandle account, char const* label, TseDuration duration, char const* const* inputLabels, size_t inputCount, char const* key);
void Account::registerFormulaProcessor(std::string key, FormulaProcessor processor) &;
void Account::addPatternFormulaByKey(std::string label, Duration duration, std::vector<std::string> inputLabels, std::string key) &;
Account.register_formula_processor(key, processor)
Account.add_pattern_formula_by_key(label, duration, input_labels, key)
Version 5.0.0.1