Pre-Trade Risk Pipeline: KYC, Margin, Max Qty & Circuit Limits
A monolithic 200-line `validateOrder()` function tangles KYC checks, tick size validation, margin calculations, and tier-specific exceptions into nested `if / else` blocks. A bug in an institutional quantity exemption accidentally bypassed the margin check, routing under-collateralized orders directly to the exchange.
Every new risk rule (e.g. leverage authorization or foreign investor limits) forces edits to the central validateOrder() function, risking regression in existing checks.
Pass requests along a dynamic chain of handlers, where each handler evaluates its own rule and either short-circuits with an error or delegates to the next handler.
We define a RiskCheck base class holding a std::unique_ptr<RiskCheck> next_. Concrete handlers (KycCheck, MarginCheck, MaxQtyCheck, CircuitBandCheck) each focus on one invariant. If a rule fails, the handler immediately returns std::unexpected("reason"). If valid, it forwards to checkNext(). Different account pipelines (Retail, Institutional, Crypto) are assembled simply by linking different combinations of handlers.
In Sprint 1 (B1 Β· Builder) we built validated TradeOrder values, and in Sprint 2 (B2 Β· Singleton) we centralized process-wide exchange limits via ExchangeConfig. Now, incoming orders must be evaluated against these limits before they can touch the live market.
We are building a pre-trade risk check gateway that validates compliance across KYC verification, account margin balance, max order quantity caps, and circuit price bands.
A monolithic 200-line validateOrder() function tangles KYC checks, tick size validation, margin calculations, and tier-specific exceptions into nested if / else blocks. A bug in an institutional quantity exemption accidentally bypassed the margin check, routing under-collateralized orders directly to the exchange.
With Chain of Responsibility (RiskCheck), each invariant lives in an independent link (KycCheck, MarginCheck, MaxQtyCheck, CircuitBandCheck) that can short-circuit immediately or delegate to next_, enabling dynamic pipelines per account tier. Next in Sprint 4 (B4 Β· Command), risk-approved orders must be executed against the OrderBook with full regulatory audit logging and batch rollback (undo) when a risk tripwire fires.
The product ticket
Before an equity order reaches the market matching engine, it must pass a strict pre-trade risk check pipeline: 1) KYC verification (account active/compliant), 2) Margin balance (cash + collateral covers price * qty), 3) Max single-order quantity cap (retail limit e.g. 10,000 shares), and 4) Circuit price band (within Β±10% of last reference price).
- β’
Process validations sequentially: if any check fails, immediately short-circuit with a clear rejection reason and avoid running downstream expensive checks.
- β’
Support differing pipeline configurations by account tier: Retail accounts require all 4 checks, Institutional accounts skip the retail quantity cap, and Crypto desks add a 5th leverage authorization check.
- β’
Allow risk rules to be added, removed, or reordered without editing existing check classes or core trading engine code.
The compliance team introduced a new margin check and an account-tier branching rule. In the naive codebase, developers added nested if / else statements to a 200-line validateOrder() function. A bug in the institutional quantity exemption accidentally bypassed the margin check, allowing an under-collateralized order to hit the exchange.
Step 1 β The naive code & why it breaks
A monolithic validateOrder() function uses a maze of nested if/else statements and boolean flags (isInstitutional, isCrypto, skipQtyCheck) to handle every validation rule in a single place.
#include <cmath>
#include <print>
#include <string>
enum class AccountTier { Retail, Institutional, Crypto };
struct Order {
std::string symbol;
int qty;
double price;
};
struct Account {
std::string id;
bool kycVerified{false};
double marginBalance{0.0};
AccountTier tier{AccountTier::Retail};
};
// Naive implementation: Monolithic validator with tangled if-else branches and tier flags.
// Adding or customizing a check requires modifying this function every time.
bool validateOrder(const Order& order, const Account& account, double refPrice,
bool hasLeverageAuth, std::string& error) {
// 1. KYC verification
if (!account.kycVerified) {
error = "KYC verification pending";
return false;
}
// 2. Margin balance check
double cost = order.qty * order.price;
if (account.marginBalance < cost) {
error = "Insufficient margin balance";
return false;
}
// 3. Max single-order quantity (Retail only!)
if (account.tier == AccountTier::Retail) {
if (order.qty > 10'000) {
error = "Retail order exceeds max cap of 10,000 shares";
return false;
}
}
// 4. Circuit price band (within +-10% of reference price)
double deviation = std::abs(order.price - refPrice) / refPrice;
if (deviation > 0.10) {
error = "Order price outside 10% circuit band";
return false;
}
// 5. Crypto leverage check (Crypto accounts only!)
if (account.tier == AccountTier::Crypto) {
if (!hasLeverageAuth) {
error = "Crypto account lacks leverage authorization";
return false;
}
}
error = "Approved";
return true;
}
int main() {
Account retail{"ACC-101", true, 50'000'000.0, AccountTier::Retail};
Order largeOrder{"RELIANCE", 15'000, 2450.0};
std::string err;
// Fails on Retail due to hardcoded 10,000 limit
if (!validateOrder(largeOrder, retail, 2450.0, false, err)) {
std::println("Retail order rejected: {}", err);
}
}Every new risk rule (e.g. leverage authorization or foreign investor limits) forces edits to the central validateOrder() function, risking regression in existing checks.
Conditionals like if (tier == Retail) ... else if (tier == Institutional) are scattered throughout the function, making it difficult to verify which checks apply to which tier.
Check order is hardcoded into the function. Reordering checks (e.g. running fast KYC before expensive margin calculations) requires rewriting and re-testing the entire validation flow.
Step 2 β The fix: Chain of Responsibility
Pass requests along a dynamic chain of handlers, where each handler evaluates its own rule and either short-circuits with an error or delegates to the next handler.
We define a RiskCheck base class holding a std::unique_ptr<RiskCheck> next_. Concrete handlers (KycCheck, MarginCheck, MaxQtyCheck, CircuitBandCheck) each focus on one invariant. If a rule fails, the handler immediately returns std::unexpected("reason"). If valid, it forwards to checkNext(). Different account pipelines (Retail, Institutional, Crypto) are assembled simply by linking different combinations of handlers.
RiskCheck β defines setNext() and the pure virtual check(order, account) returning std::expected<void, std::string>.
KycCheck, MarginCheck, MaxQtyCheck, CircuitBandCheck, CryptoLeverageCheck β validate specific invariants and forward or short-circuit.
The risk gateway that composes custom pipelines per account tier and feeds orders into the head of the chain.
Unlock Full UML Diagram & C++23 Solution
The interactive UML architecture diagrams, refactored C++23 code, real-world extensions, and interview Q&A are part of Brute2Best Pro. All 7 foundation chapters are 100% free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.