3. Risk management
Every robot in this engine trades against a constraint, and the first architectural decision you make about a strategy is where that constraint is evaluated: inside the engine, on your own machine, against your own view of the portfolio — or at the venue, inside a protective order that has already left you. The engine supports both, they can be mixed freely on one account, and they are not two spellings of the same thing. This chapter states the choice, then documents each side; the exhaustive builder reference stays in the Account chapter for policies and in the Rule chapter for rules.
Two places where risk can live
Risk computed inside the engine. Here the account holds the constraint and the engine is the authority. A risk policy (tse_add_risk_policy) is a named, account-attached limit checked before an order is allowed out: the engine reserves the order's exposure, tests the policy against the resulting portfolio, and refuses the order if the policy does not hold. A risk rule (tse_add_rule_risk) is the second form: it subscribes to a contract's exposure and emits a closing order when the position's unrealized-P&L ratio crosses a threshold. Both see the whole portfolio, both are reproduced bit for bit in a backtest — the very same code runs there — and both are alive only as long as your process is. A stop that lives in the engine protects nothing while the engine is down.
Risk delegated to the venue. A bracket (tse_add_rule_bracket) and a one-cancels-other pair (tse_add_rule_oco) do not keep the decision. They emit protective orders with their trigger levels already priced and hand them to the execution point — the Simulator in a backtest, the exchange in production. From that moment the protection is out of your hands in both senses: you cannot recompute it from portfolio state, and it no longer depends on your process being alive. A resting stop survives a crash, a disconnection and a restart. What you give up is exactly what you gained on the other side: the venue sees one contract and one price, never the portfolio, and it cannot refuse an order — it can only execute a level you fixed in advance.
| Property | Computed inside the engine | Left at the venue |
|---|---|---|
| Builders | tse_add_risk_policy, tse_add_rule_risk | tse_add_rule_bracket, tse_add_rule_oco |
| What arms it | attachment to the account; subscription to the exposure | a pattern firing |
| What it watches | the position's unrealized-P&L ratio; the portfolio aggregate | a price level around the entry fill |
| Scope | one contract or the whole portfolio | one contract |
| Can refuse an order before it leaves | yes, a policy does exactly that | no |
| Survives the loss of your process | no | yes |
Two differences between the two sides must be read carefully, because they are what makes a number copied from one side to the other wrong.
A threshold on one side is not a threshold on the other. The thresholdRatio of a risk rule is a ratio of the position's unrealized P&L: the engine forms r as the contract's unrealized P&L divided by its acquisition value and compares that against the threshold. The ratio of a venue risk leg is measured in price space around the entry fill: for a long position the stop trigger is priced at entry - entry * ratio and the take-profit at entry + entry * ratio, mirrored for a short. The two are different measurements of different quantities, and a value tuned on one side has to be re-derived for the other, never ported.
A bracket needs a pattern to arm it; a risk rule needs none. tse_add_rule_bracket and tse_add_rule_oco are market-channel rules: both take a patternLabel, and they emit nothing until that pattern fires. tse_add_rule_risk takes no pattern label at all — it watches the exposure directly and fires on what the position does, whatever the market is printing. A venue-resting protection is therefore always placed by a signal; an engine-side protection is permanently attached to the position.
Risk computed inside the engine
The pre-trade gate
A risk policy is a named constraint attached to the account and evaluated at submit time. Three kinds exist, selected by TseRiskPolicyType: tse_risk_policy_value limits the market value of a position or of the whole portfolio, tse_risk_policy_quantity limits a position's quantity and is per-contract only, and tse_risk_policy_time_period is a trading window rather than a magnitude.
A threshold policy holds while <metric> <comparison> threshold. The quantity policy is meaningful per contract only — attached portfolio-wide it never holds, because quantities of different instruments are not summable.
The time-period policy is a repeating trading window: cyclePeriodNs is the cycle length, offsetNs the window start inside the cycle and activeDurationNs the window length, all in nanoseconds, and the policy holds while local wall-clock time falls inside the active window. timeZoneName is an IANA zone name; an empty name selects the host machine's zone, and an unknown name fails the call with a diagnostic.
The enforcement rule is asymmetric, and this is the property to build on. While an attached policy does not hold, submissions and amendments that would increase exposure are refused: the transaction is rolled back, never reaches the execution, and the refusal is logged with the policy's diagnostic. Transactions that do not increase exposure — reductions and closes — are never blocked. A robot can therefore always trade out of a position a policy has capped, and a breached limit can never trap you inside the position it was meant to bound.
The refusal is atomic on the robot side as well: every reservation the submission took, including all legs of a multileg, is rolled back together, and the robot stays active and processes the next signal normally. The Robot chapter describes that gate among the other places an update can be dropped; the Account chapter carries the full policy reference.
Scope is chosen by the symbol: naming a contract makes the policy per-contract, while the value that makes it portfolio-wide is spelled differently in each language.
| Language | Portfolio-wide contractSymbol |
|---|---|
| C | NULL |
| C++ | an empty string |
| Python | None |
TseStatus tse_add_risk_policy(TseAccountHandle account, char const* label, TseRiskPolicyType policyType,
char const* contractSymbol, double threshold, TseCmp comparison);
TseStatus tse_add_risk_policy_time_period(TseAccountHandle account, char const* label, char const* contractSymbol,
int64_t cyclePeriodNs, int64_t offsetNs,
int64_t activeDurationNs, char const* timeZoneName);
TseStatus tse_remove_risk_policy(TseAccountHandle account, char const* label, char const* contractSymbol);
account.addRiskPolicy("MaxValue", tse::RiskPolicy::value, 1'000'000.0, tse::Cmp::le);
account.addRiskPolicyTimePeriod("Session", "WTI", dayNs, openOffsetNs, activeNs, "America/New_York");
account.removeRiskPolicy("MaxValue");
account.add_risk_policy("MaxValue", tse.RiskPolicy.Value, 1_000_000.0, tse.Cmp.Le)
account.add_risk_policy_time_period("Session", "WTI", day_ns, open_offset_ns, active_ns, "America/New_York")
account.remove_risk_policy("MaxValue")
The position watchers
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 and tse_rule_take_profit_trailing. The rule is bound to the named contract's exposure and takes no pattern.
thresholdRatio is a positive magnitude compared against the position's unrealized-P&L ratio; a zero threshold never fires. Writing that ratio as r and the threshold as theta, the four laws are:
- 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 extreme of the position's mark and fires when the retreat from that extreme reaches
theta. - Trailing take-profit first arms — the extreme must itself have travelled
thetain the position's favour — and only then fires on a retreat ofthetafrom it.
The order the firing emits is described by the same TseRuleParams every trade rule uses: a stop on a long is typically txnSide short, posSide long, quantity mode tse_quantity_all. The posSide guard applies here exactly as elsewhere — a rule declared for a side the portfolio does not hold simply never fires. The Rule chapter documents the parameter structure field by field.
TseStatus tse_add_rule_risk(TseAccountHandle account, char const* label, TseRuleType ruleType,
TseRuleParams const* params, double thresholdRatio, char const* contractSymbol);
account.addRuleRisk("Stop", tse::RuleType::stop_loss, params, 0.02, "WTI");
params = tse.make_rule_params(tse.Quantity.All, 0.0, tse.Price.Market, 0.0, 0.0, 0.0,
tse.Side.Short, tse.Side.Long, tse.Tif.Day, 0)
account.add_rule_risk("Stop", tse.RuleType.StopLoss, params, 0.02, "WTI")
Risk left at the venue
A bracket is an entry order that carries its own protection; an OCO is that protection placed alone, on a position that already exists. Both are built from one specification: a pair of optional risk legs and the time-in-force both resting orders share.
The protection is materialized from the position, not from the rule. Nothing rests at the venue at the moment the rule is built, and nothing rests there when the pattern merely fires. The engine waits for a fill on the contract, reads the position's side, open quantity and acquisition price from the exposure, prices the legs off that acquisition price, sizes them to the open quantity and sends them out as non-replaceable market orders on the side opposite the position. A fixed leg carries an absolute trigger level; a trailing leg carries the ratio and is re-priced against the running extreme. On a partial fill the pair is re-materialized: the previous legs are cancelled and replaced by legs priced and sized against the new position. An OCO fired while the contract is flat has nothing to protect and is refused with no open position to protect.
One-cancels-other is the sibling law. When one leg of a pair executes, the engine cancels the other in the same step. A bracket whose specification carries both legs produces exactly the same linked pair as an OCO, attached to its entry.
What the Simulator does with a resting protective order. In a backtest the Simulator stands in for the venue. It accepts the protective leg, opens a trigger state for it — the running price extreme on the position's side, plus an armed flag for the trailing take-profit — and from then on tests the trigger on every market event for the contract, reading the price from the same field its fill model uses for that side (the OHLCV and bid/ask preferences of TseSimulatorConfig). While the trigger has not fired the order is not executable at all, however reachable its price would otherwise look. A fixed stop fires when the read price falls to or through its level for a long and rises to or through it for a short; a fixed take-profit fires on the mirror condition; a trailing leg fires when the retreat of the price from the observed extreme, as a ratio of that extreme, reaches the leg's ratio. When the order is fully executed its trigger state is discarded.
Under a custom execution the same legs are handed to your fill callback as ordinary TseOrder records the moment they are created, carrying their computed trigger level in limitPrice while priceType reads tse_price_market. Translating that into whatever resting order type your venue actually offers is the integration's responsibility; the environment chapter describes the custom-execution seam, and the examples chapter runs a fixed bracket, a trailing bracket and an OCO end to end.
The specification itself, TseVenueRiskSpec, is one structure holding the two legs and the shared time-in-force.
| Field | Type |
|---|---|
| stopLoss | TseVenueRiskLeg |
| takeProfit | TseVenueRiskLeg |
| tif | TseTif |
Each leg in turn, TseVenueRiskLeg, is a presence flag, a threshold flavor and a ratio.
| Field | Type | Meaning |
|---|---|---|
present | int32_t | zero: the leg is absent and the rest of the struct is ignored |
kind | int32_t | 0 fixed, 1 trailing; any other value is rejected |
ratio | double | the threshold ratio, in price space around the entry fill |
At least one leg must be present — a specification with both legs absent is rejected, a stop-only or take-only specification is legal. Every ratio must be strictly positive; a trailing ratio, and any ratio that would price its trigger through zero (a stop under a long, a take-profit under a short), must additionally stay below 1. A degenerate ratio is refused with a diagnostic rather than silently clamped.
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);
tse::VenueRiskSpec const risk {tse::VenueRiskLeg {true, 0, 0.02}, tse::VenueRiskLeg {true, 1, 0.05}, tse::Tif::gtc};
account.addRuleBracket("Bracket", entryLeg, risk, "PatternToLong");
account.addRuleOco("Oco", "WTI", risk, "PatternAtNoon");
risk = tse.make_venue_risk_spec(tse.make_venue_risk_leg(True, 0, 0.02),
tse.make_venue_risk_leg(True, 1, 0.05), tse.Tif.Gtc)
account.add_rule_bracket("Bracket", entry_leg, risk, "PatternToLong")
account.add_rule_oco("Oco", "WTI", risk, "PatternAtNoon")
Version 5.0.0.0