6. Rule
A rule is the bridge between observation and action: it listens to one event source and, on each firing, emits one fully specified instruction — an order, an amend of an in-flight order, or a whole transaction group. Everything an order will carry — contract, side, quantity mode, price form, slippage, fee, time-in-force — is fixed at rule construction, except that three of those may be declared undefined and then come from the signal on every firing: the quantity, the price and the transaction side. All rule builders live in tse_rule.h, attach to an account handle, and every one of them requires a unique rule label: the label is how a robot later collects its rules (tse_add_robot), and it travels on every trade the rule produces (the ruleLabel field of TseRetained and TseTrade).
The rule channels
Rules split into two channels by what they observe.
- Market-event-driven rules watch a pattern. The builders
tse_add_rule_market,tse_add_rule_multileg,tse_add_rule_bracket,tse_add_rule_oco,tse_add_rule_cancel,tse_add_rule_replaceandtse_add_rule_modifyall take apatternLabelargument and fire when — and only when — that pattern fires. - Position-risk-driven rules watch the position itself. The builder
tse_add_rule_risktakes no pattern label at all: the rule subscribes to the exposure of the named contract and fires when the position's unrealized-P&L ratio crosses its threshold, regardless of what any pattern is doing.
Selectivity in the market channel is strict and structural. Each rule names exactly one pattern, and the subscription is keyed by the identifier derived from that pattern label — firings of every other pattern never reach the rule. Several rules may name the same pattern; each of them receives every firing of it independently. The niceValue field of TseRuleParams orders observers of the same pattern: it is the rule's priority within the robot graph.
Trade rules
There are no separate entry or exit builders. tse_add_rule_market is the single builder for all market-event trade rules, and the rule kind is a value of TseRuleType passed to it: tse_rule_entry, tse_rule_exit or tse_rule_rebalance. Passing a risk value (tse_rule_stop_loss, tse_rule_stop_loss_trailing, tse_rule_take_profit, tse_rule_take_profit_trailing) to this builder is an error, exactly as passing a market value to tse_add_rule_risk is.
The full trade specification is one flat structure, mirroring the engine's rule constructor. All fields are required; the wrappers supply no defaults.
| Field | Type |
|---|---|
| quantityMode | TseQuantityMode |
| quantity | double |
| priceType | TsePriceType |
| limitPrice | double |
| slippage | double |
| fee | double |
| txnSide | TseSide |
| posSide | TseSide |
| tif | TseTif |
| niceValue | int32_t |
What the signal may supply. Three of the fields have an undefined form, and declaring one undefined is how a rule says that the signal brings it. On every firing the rule takes each undefined field out of the order the pattern published and leaves every other field exactly as declared; a rule that declared all three never reads the order at all. A risk rule reads nothing from a signal and is rejected at build time if any of the three is undefined. The Pattern chapter describes the order itself.
Quantity modes. Three values of TseQuantityMode are accepted; any other value is rejected, the former numeric 3 (tse_quantity_from_signal, now gone) among them.
| Mode | Meaning |
|---|---|
tse_quantity_fixed | every firing trades the quantity given in the params |
tse_quantity_all | every firing closes the whole position on the contract; quantity is ignored |
tse_quantity_undefined | the per-fire quantity is read from the order the signal carries; quantity is ignored. A signal bringing zero names no size, and the rule then issues nothing and logs the reason |
The same pattern can therefore drive differently sized orders on every fire, and nothing carries over: a firing whose signal brings no quantity does not reuse the previous one.
Price forms. tse_price_market places a market order and ignores limitPrice; tse_price_limit places a limit order at limitPrice; tse_price_undefined takes the price from the order the signal carries, where a zero means at market and any other value a limit at that price. tse_price_general is rejected in a rule (see the last section). slippage and fee ride on every order the rule emits.
Sides. txnSide is the side of the transaction the rule issues: tse_side_long, tse_side_short, or tse_side_undefined to take it from the order the signal carries. A neutral txnSide is rejected — a transaction without a direction is not a transaction — and so is any value outside the three position sides. posSide is a guard: the portfolio side that must hold for the rule to fire — tse_side_neutral for an entry into a flat book, tse_side_long for a rule that closes a long, and so on. It is never taken from a signal, and it must be one of the three position sides.
Time-in-force. tif is a TseTif value: tse_tif_day, tse_tif_gtd, tse_tif_gtc, tse_tif_gts, tse_tif_atc, tse_tif_opg, tse_tif_fok, tse_tif_ioc or tse_tif_aon. The C++ mirror is tse::Tif, the Python mirror is the Tif IntEnum (Tif.Day … Tif.Aon).
Transaction kind (TseTxnType) and order priority (TsePriority) do not appear in TseRuleParams: for a single-contract rule the engine derives the transaction kind from the rule type. Those two enums are per-leg properties of multileg rules and are described below.
Rebalance. A rebalance firing is not a bring-to-target instruction and computes no distance to any desired size: it emits one order for exactly the quantity its params resolve, on the rule's txnSide, and the engine types the result as a chaining transaction (tse_txn_chaining) — an adjustment of an existing position. Each fill shifts the position by that amount, and a fill on the side already held re-averages the acquisition price: an entry of 4 at 100 followed by two rebalance fills of 2 at 103 and at 108 leaves a position of 8 whose acquisition price is the volume-weighted average of the three fills, and no realized profit is booked on the same-side adds. The three trade kinds form a validated set: an entry initiates a position from flat, a rebalance grows or partially shrinks the position or carries it through flat to the opposite side, and an exit with tse_quantity_all flattens it on a return-to-flat or a flip — which arms the entry again.
The builder, in C, C++ and Python:
TseStatus tse_add_rule_market(TseAccountHandle account, char const* label,
TseRuleType ruleType, TseRuleParams const* params,
char const* patternLabel, char const* contractSymbol);
void Account::addRuleMarket(std::string label, RuleType ruleType, RuleParams const& params,
std::string patternLabel, std::string contractSymbol) &;
params = tse.make_rule_params(quantity_mode, quantity, price_type, limit_price,
slippage, fee, txn_side, pos_side, tif, nice_value)
account.add_rule_market(label, rule_type, params, pattern_label, contract_symbol)
Risk rules
tse_add_rule_risk builds the four position-watching rules: tse_rule_stop_loss, tse_rule_take_profit and their trailing variants tse_rule_stop_loss_trailing, tse_rule_take_profit_trailing. A risk rule is bound to the named contract's exposure, not to any pattern — the market can print any shape it likes; the rule reacts only to what the position's unrealized P&L does.
thresholdRatio is a positive magnitude the engine compares the position's unrealized-P&L ratio against. A zero threshold never fires. Writing the ratio as r and the threshold as theta:
- Fixed stop-loss fires when the loss reaches the threshold: r <= -theta.
- Fixed take-profit fires when the gain reaches the threshold: r >= +theta.
- Trailing stop-loss tracks the running peak of the ratio and fires when the retreat from that peak reaches theta.
- Trailing take-profit first arms — the running peak must itself reach theta — and then fires when the ratio retreats from the peak by theta.
The trade the firing emits is described by the same TseRuleParams: a stop-loss on a long position is typically txnSide short, posSide long, tse_quantity_all. The posSide guard applies to risk rules exactly as to market rules.
The builder, in C, C++ and Python:
TseStatus tse_add_rule_risk(TseAccountHandle account, char const* label,
TseRuleType ruleType, TseRuleParams const* params,
double thresholdRatio, char const* contractSymbol);
void Account::addRuleRisk(std::string label, RuleType ruleType, RuleParams const& params,
double thresholdRatio, std::string contractSymbol) &;
account.add_rule_risk(label, rule_type, params, threshold_ratio, contract_symbol)
Order mutations
Three builders generate amendments of in-flight orders instead of new trades. Each is a market-event rule: it names a pattern, and on every firing it emits one amend envelope for the named contract. The rule does not name a specific order id — the engine routes the envelope to the order currently in flight for that contract.
All three share one signature; what differs is which arguments the envelope reads.
| Builder | Effect on the in-flight order | Reads |
|---|---|---|
tse_add_rule_cancel | pulls the order | neither quantity nor price |
tse_add_rule_modify | changes the remaining quantity, keeps the price | quantity only |
tse_add_rule_replace | re-places the order with a new quantity and a new price | both |
The engine additionally rejects a modify or replace whose quantity is not positive.
The three builders, in C, C++ and Python:
TseStatus tse_add_rule_cancel (TseAccountHandle account, char const* label, char const* contractSymbol,
double quantity, double price, char const* patternLabel);
TseStatus tse_add_rule_replace(TseAccountHandle account, char const* label, char const* contractSymbol,
double quantity, double price, char const* patternLabel);
TseStatus tse_add_rule_modify (TseAccountHandle account, char const* label, char const* contractSymbol,
double quantity, double price, char const* patternLabel);
void Account::addRuleCancel (std::string label, std::string contractSymbol, double quantity, double price, std::string patternLabel) &;
void Account::addRuleReplace(std::string label, std::string contractSymbol, double quantity, double price, std::string patternLabel) &;
void Account::addRuleModify (std::string label, std::string contractSymbol, double quantity, double price, std::string patternLabel) &;
account.add_rule_cancel(label, contract_symbol, quantity, price, pattern_label)
account.add_rule_replace(label, contract_symbol, quantity, price, pattern_label)
account.add_rule_modify(label, contract_symbol, quantity, price, pattern_label)
Multileg
tse_add_rule_multileg builds a rule that, on each pattern firing, emits all its legs as one atomic multileg transaction. Legs are passed as an array of descriptors; each leg names its own contract and carries the complete per-order specification.
| Field | Type |
|---|---|
| contractSymbol | char[32] |
| quantityMode | TseQuantityMode |
| quantity | double |
| priceType | TsePriceType |
| limitPrice | double |
| slippage | double |
| fee | double |
| txnType | TseTxnType |
| txnSide | TseSide |
| posSide | TseSide |
| tif | TseTif |
| priority | TsePriority |
A leg's quantityMode accepts tse_quantity_fixed (uses quantity) or tse_quantity_all (resolved by the engine at fire time); a leg takes nothing from a signal, so the undefined forms are not available for legs. txnType is the transaction kind of the leg (tse_txn_enter, tse_txn_exit, tse_txn_stoploss, tse_txn_stoploss_trailing, tse_txn_takeprofit, tse_txn_takeprofit_trailing, tse_txn_chaining, tse_txn_forced — the numeric values are the engine codes), and priority marks the leg's order as tse_priority_replaceable or tse_priority_non_replaceable.
A multileg transaction carries between one and eight legs. A legCount outside these bounds fails loud: the engine rejects it with an error naming the range.
Atomic net-price fills. A multileg transaction is filled as a unit or not at all. When the transaction is built, the engine derives a net price and per-leg weights from the legs' sides, quantities and limit prices. The leg orders then rest as one group: on every market event touching any of the group's contracts, the engine reads the currently reachable price of every leg — a group with even one leg lacking market data stays untouched — combines them with the same weights, long legs adding and short legs subtracting, and compares the result against the net-price bound. Only when the bound is satisfied does the group execute, and then every leg fills in full at its reachable price in the same instant; legs are never filled piecemeal, and a partial group is never left behind. A spread that never becomes reachable simply rests forever.
The builder, in C, C++ and Python:
TseStatus tse_add_rule_multileg(TseAccountHandle account, char const* label,
TseLegDescriptor const* legs, size_t legCount,
char const* patternLabel);
void Account::addRuleMultileg(std::string label, std::vector<LegDescriptor> const& legs, std::string patternLabel) &;
leg = tse.make_leg_descriptor(contract_symbol, quantity_mode, quantity, price_type, limit_price,
slippage, fee, txn_type, txn_side, pos_side, tif, priority)
account.add_rule_multileg(label, [leg_a, leg_b], pattern_label)
Bracket and OCO
A bracket is an entry order that carries its own venue-resting protection. tse_add_rule_bracket takes an entry leg — a full TseLegDescriptor — plus a venue-risk specification, and on each pattern firing emits the entry together with the resting stop-loss / take-profit orders derived from it.
The specification is a pair of optional risk legs and one time-in-force that both resting orders share. At least one leg must be present.
| Field | Type |
|---|---|
| stopLoss | TseVenueRiskLeg |
| takeProfit | TseVenueRiskLeg |
| tif | TseTif |
| Field | Type |
|---|---|
| present | int32_t |
| kind | int32_t |
| ratio | double |
In TseVenueRiskLeg, present encodes the engine's optional — zero means the leg is absent and the remaining fields are ignored; kind selects the threshold flavor, 0 for fixed and 1 for trailing (any other value is rejected); ratio is the threshold ratio, with the same fixed/trailing semantics as in the risk-rule section above, applied in price space around the entry fill.
tse_add_rule_oco emits the protection pair without an entry: on each firing it places the one-cancels-other pair described by the specification for the named contract, guarding a position that already exists. The sibling semantics are what the name says — the two resting orders are linked, and the execution of one causes the engine to cancel the other. A bracket whose specification carries both legs produces the same linked pair, attached to its entry.
The two builders, in C, C++ and Python:
TseStatus tse_add_rule_bracket(TseAccountHandle account, char const* label,
TseLegDescriptor const* entry, TseVenueRiskSpec const* risk,
char const* patternLabel);
TseStatus tse_add_rule_oco(TseAccountHandle account, char const* label,
char const* contractSymbol, TseVenueRiskSpec const* risk,
char const* patternLabel);
void Account::addRuleBracket(std::string label, LegDescriptor const& entry, VenueRiskSpec const& risk, std::string patternLabel) &;
void Account::addRuleOco(std::string label, std::string contractSymbol, VenueRiskSpec const& risk, std::string patternLabel) &;
sl = tse.make_venue_risk_leg(True, 0, 0.02)
tp = tse.make_venue_risk_leg(True, 1, 0.05)
risk = tse.make_venue_risk_spec(sl, tp, tse.Tif.Gtc)
account.add_rule_bracket(label, entry_leg, risk, pattern_label)
account.add_rule_oco(label, contract_symbol, risk, pattern_label)
Extended sides and prices
TseSide is a full mirror of the engine's side/quotation enumeration, and the numeric values are the engine codes — deliberately powers of two, so that combinations of directed sides never collide with a named value. tse_side_long carries the engine aliases bid/buy, tse_side_short carries ask/sell.
| Value | Code | Where it matters |
|---|---|---|
tse_side_neutral | 1 | a flat position; the posSide guard of an entry rule |
tse_side_long | 2 | order and position side; the bid side of a book |
tse_side_short | 4 | order and position side; the ask side of a book |
tse_side_mid | 8 | the quotation of a neutral position: exposure marking is side-dependent — long marks against the bid, short against the ask, neutral against the mid |
tse_side_trade | 16 | tags values that originate from executed trades — trade prints and actual (traded) prices in the engine's price algebra |
tse_side_quote | 32 | tags quoted prices — resting order prices and notionals; a market-priced rule order is typed as a quote-side price |
tse_side_general | 64 | the provenance-erased tag: bid and ask are cast to general before being combined (e.g. into a mid), so values of different origins can meet in one expression |
Only the three position sides — neutral, long, short — are legal in txnSide and posSide of a rule or a leg, with the two exceptions already stated: a rule may declare txnSide undefined and read it from the signal, and a neutral txnSide is refused even though neutral is a position side. The engine rejects everything else there. The extended values surface elsewhere: the side field of a trade tick (TseTickTrade), the side column of a CSV load (the parser accepts all eight names and their numeric codes), the exposure snapshots, and the string round-trip tse_side_to_string. The C++ mirror is tse::Side (long_, short_, mid, trade, quote, general), the Python mirror is Side (Side.Long, Side.Mid, …).
TsePriceType carries one value that never places an order: tse_price_general (PriceType::general in C++, Price.General in Python). A rule or a leg handed it is refused at build time; tse_price_undefined is accepted in a rule and means that the price comes from the signal, so of the four values a rule takes three. The general value exists because the engine's price algebra produces provenance-erased prices — a ratio of two prices, a trailing extreme tracked across bars — and the C mirror must be able to represent every engine value that can cross the boundary, such as the priceType field of a retained trade.
Version 5.0.0.1