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; the firing only supplies the moment and, in one specific mode, the quantity. 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 |
Quantity modes. Three values of TseQuantityMode are accepted; any other value, including tse_quantity_undefined, is rejected.
| 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_from_signal | the per-fire quantity is read from the value the pattern publishes with its firing; quantity is ignored |
In the from-signal mode the rule takes the quantity from the signal payload at the moment of firing, when that payload is present and positive — the same pattern can therefore drive differently sized orders on every fire.
Price forms. tse_price_market places a market order and ignores limitPrice; tse_price_limit places a limit order at limitPrice. These are the two placement forms — the remaining enum values do not create a third kind of order (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 and must be tse_side_long or tse_side_short. 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. The engine rejects a rule whose sides are not position sides (neutral, long, short).
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 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 while it stays on the same 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); the from-signal mode is 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; the engine rejects the rest 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 beyond the two placement forms: tse_price_general (PriceType::general in C++, Price.General in Python). It never places an order — in TseRuleParams only tse_price_market and tse_price_limit are meaningful, and anything other than limit is treated as a market order. The 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.0