Pre-Trade Risk Checks
Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.
Orders Need Gates
Why this lesson matters
In Order Management & FIX Integration, you saw that a strategy does not send directly to the exchange. It asks the OMS to create, track, and encode orders.
Now we zoom into one OMS responsibility: pre-trade risk checks.
A strategy may generate a bad order because of:
- a bug in signal logic
- stale or missing reference data
- a bad config value
- a loop that sends too many orders
- an unexpected position or open-order state
The risk gate’s job is simple:
Strategy intent -> Pre-trade risk -> OMS state -> FIX Gateway -> Exchange
reject here if unsafe
A fast trading system is only useful if it refuses dangerous orders before they leave your process.

Risk Gate Map
What gets checked?
A practical pre-trade risk layer combines several small checks. Each one answers a narrow question.
OrderIntent
|
+-> Symbol allowed?
+-> Side and quantity valid?
+-> Price sane?
+-> Notional below limit?
+-> Position after order below limit?
+-> Open-order exposure below limit?
+-> Order rate below limit?
+-> Kill switch enabled?
Two habits matter:
- Fail closed: if required information is missing, reject the order.
- Return a reason: every rejection should be explainable later.
This is not only a trading concern. It is also a debugging concern. When a strategy stops trading, you want to know whether it stopped because there were no signals or because the risk gate rejected everything.

Check Location
Quick check
A strategy produces an order intent: BUY 10,000 XYZ @ 250.00.
Where should pre-trade risk checks generally run?
Basic Risk Engine
A first C++ risk gate
Start with a clear reference implementation. This is not the final low-latency layout, but it makes the rules obvious.
We will check:
- symbol is allowed
- quantity is positive and below max
- price is positive
- order notional is below max
- price is within a band around a reference price
- kill switch is not active
Notice that the function returns a structured result instead of just true or false. That reason string is valuable when debugging OMS behavior.
```cpp
#include <cmath>
#include <iostream>
#include <string>
#include <unordered_set>
struct OrderIntent {
std::string symbol;
char side; // 'B' buy, 'S' sell
int quantity;
double price;
};
struct RiskConfig {
std::unordered_set<std::string> allowedSymbols{"AAPL", "MSFT"};
int maxQty = 1000;
double maxNotional = 100000.0;
double maxPriceMovePct = 0.05; // 5% away from reference
bool killSwitch = false;
};
struct RiskResult {
bool accepted;
std::string reason;
};
RiskResult checkOrder(const OrderIntent& o, const RiskConfig& cfg, double referencePrice) {
if (cfg.killSwitch) {
return {false, "kill switch active"};
}
if (!cfg.allowedSymbols.contains(o.symbol)) {
return {false, "symbol not allowed"};
}
if (o.side != 'B' && o.side != 'S') {
return {false, "invalid side"};
}
if (o.quantity <= 0 || o.quantity > cfg.maxQty) {
return {false, "quantity outside limit"};
}
if (o.price <= 0.0 || referencePrice <= 0.0) {
return {false, "invalid price or reference"};
}
const double notional = o.quantity * o.price;
if (notional > cfg.maxNotional) {
return {false, "notional limit exceeded"};
}
const double movePct = std::fabs(o.price - referencePrice) / referencePrice;
if (movePct > cfg.maxPriceMovePct) {
return {false, "price band exceeded"};
}
return {true, "accepted"};
}
int main() {
RiskConfig cfg;
OrderIntent orders[] = {
{"AAPL", 'B', 100, 190.00},
{"TSLA", 'B', 100, 250.00},
{"MSFT", 'S', 2000, 410.00},
{"AAPL", 'B', 100, 250.00}
};
const double referenceAapl = 188.00;
for (const auto& o : orders) {
RiskResult r = checkOrder(o, cfg, referenceAapl);
std::cout << o.symbol << " " << o.side << " " << o.quantity
<< " @ " << o.price << " -> "
<< (r.accepted ? "ACCEPT" : "REJECT")
<< " (" << r.reason << ")\n";
}
}
```
Fail Closed
True or false?
If the risk layer is missing required reference data, it is safer to accept the order and let the exchange decide.
Position Limits
Risk is not only one order
Checking a single order is necessary, but not enough.
Suppose your current position is +900 shares and your max long position is +1000.
Current position: +900
New buy order: +200
Projected position: +1100 -> reject
For sells, the projection moves the other way:
Current position: -850
New sell order: -300
Projected position: -1150 -> reject if max short is -1000
In production, you also account for open orders that have been sent but not filled or canceled yet. Otherwise, ten accepted buy orders of 100 shares can quietly become 1000 shares of pending exposure.
We will keep the model small here: current position plus open buy/sell quantity plus the new order.

Projected Exposure Code
Check the position after the order
This example tracks a small position state for one symbol.
The key idea:
projected = current_position + open_buy_qty - open_sell_qty + signed_new_order_qty
Why include open orders? Because they can still fill. If you ignore them, the risk gate may accept too many orders before fills arrive.
```cpp
#include <iostream>
#include <string>
struct OrderIntent {
std::string symbol;
char side; // 'B' buy, 'S' sell
int quantity;
};
struct PositionState {
int currentPosition = 0;
int openBuyQty = 0;
int openSellQty = 0;
};
struct PositionLimits {
int maxLong = 1000;
int maxShort = -1000;
};
int signedQty(const OrderIntent& o) {
return o.side == 'B' ? o.quantity : -o.quantity;
}
int projectedPosition(const PositionState& s, const OrderIntent& o) {
return s.currentPosition + s.openBuyQty - s.openSellQty + signedQty(o);
}
bool withinLimits(int projected, const PositionLimits& limits) {
return projected <= limits.maxLong && projected >= limits.maxShort;
}
void test(const PositionState& state, const OrderIntent& order, const PositionLimits& limits) {
const int projected = projectedPosition(state, order);
std::cout << order.symbol << " " << order.side << " " << order.quantity
<< " projected=" << projected << " -> "
<< (withinLimits(projected, limits) ? "ACCEPT" : "REJECT") << "\n";
}
int main() {
PositionLimits limits{1000, -1000};
PositionState nearLong;
nearLong.currentPosition = 900;
nearLong.openBuyQty = 50;
nearLong.openSellQty = 0;
test(nearLong, {"AAPL", 'B', 25}, limits); // projected 975
test(nearLong, {"AAPL", 'B', 100}, limits); // projected 1050
test(nearLong, {"AAPL", 'S', 300}, limits); // projected 650
}
```
Missing Number
Fill in the blank
For a buy order, projected position generally moves upward by the order ________.
Rate And Kill Switch
Stop runaway behavior
Some failures are not about one bad price or one large quantity. They are about too many acceptable-looking orders.
Examples:
- a strategy loop emits the same order repeatedly
- network reconnect logic resends too aggressively
- cancel/replace logic starts thrashing
- a config error enables a strategy on too many symbols
Two common controls:
- Rate limit: allow only N orders per time window.
- Kill switch: immediately reject new orders when risk, ops, or monitoring disables trading.
A rate limit is not a substitute for price and position checks. It is another layer. In HFT systems, safety usually comes from multiple simple gates rather than one magical rule.
```cpp
#include <iostream>
class SimpleOrderRateLimiter {
int maxPerWindow;
int usedInWindow = 0;
public:
explicit SimpleOrderRateLimiter(int maxOrders) : maxPerWindow(maxOrders) {}
bool allow() {
if (usedInWindow >= maxPerWindow) {
return false;
}
++usedInWindow;
return true;
}
void resetWindow() {
usedInWindow = 0;
}
};
int main() {
SimpleOrderRateLimiter limiter(3);
for (int i = 1; i <= 5; ++i) {
std::cout << "order " << i << " -> "
<< (limiter.allow() ? "ACCEPT" : "REJECT rate limit")
<< "\n";
}
std::cout << "reset window\n";
limiter.resetWindow();
std::cout << "order 6 -> "
<< (limiter.allow() ? "ACCEPT" : "REJECT rate limit")
<< "\n";
}
```
Audit The Rejection
Rejections are useful data
A risk rejection should not disappear silently.
At minimum, record:
- timestamp
- strategy or component name
- symbol
- side, quantity, price
- reject reason
- relevant limits
- current position or exposure snapshot
09:30:01.123 risk_reject strategy=twap_1 symbol=AAPL side=B qty=2000
reason=quantity_outside_limit limit=1000 current_pos=350
This helps answer practical questions:
- Did the strategy stop because signals stopped?
- Did a config limit block all orders?
- Did a price reference become stale?
- Did position exposure grow faster than expected?
In Latency Instrumentation and Histograms, you learned not to print inside hot paths casually. The same applies here: design audit logging carefully. In a lab, std::cout is fine. In production, use a measured, non-blocking path.
Hot Path Shape
Keep checks predictable
Pre-trade risk sits on the outbound order path, so it must be correct and predictable.
Good habits from Low-Latency C++ for HFT still apply:
- load config before trading starts
- avoid allocation in the hot check function
- prefer simple numeric comparisons
- keep reject reasons stable and enumerable
- separate slow logging from the decision path
- test boundary cases: exactly at max qty, just above max qty, missing reference price
A common production shape is:
Startup:
load limits, symbols, account config
validate config
arm strategy only if config is sane
Hot path:
read immutable limits
read current exposure
run simple checks
accept or reject with reason code
Next, Market Data Handling & Order Book Reconstruction will build the trustworthy market state that later checks and strategies can consume.

Risk Gate Checklist
Final check
Which design is strongest for a beginner-friendly pre-trade risk layer?
One Pager Cheat Sheet
One Pager Cheat Sheet
- Pre-trade risk checks reject unsafe order intent before the OMS sends anything outward.
- Risk gates should be small, explicit, fail-closed checks that return clear reject reasons.
- Pre-trade risk belongs before outbound order encoding and transmission.
- A risk check should return both accept/reject and a durable reason for audit and debugging.
- When required risk inputs are missing, fail closed rather than sending unknown risk.
- Position checks must consider projected exposure, not just the latest order in isolation.
- Projected exposure should include current position, live open orders, and the new order.
- Buy orders add quantity to projected position; sell orders subtract quantity.
- Rate limits and kill switches protect against runaway order flow, not just individual bad orders.
- Every risk rejection should be auditable with reason, order details, and relevant state.
- Pre-trade checks should be preconfigured, allocation-light, deterministic, and tested at boundaries.
- A strong risk layer is explicit, fail-closed, and explainable with stable reject reasons.