A candidate posting about a Citadel software engineer loop in early 2026 described the second-round day as feeling like a compressed internship: an hour of system design, two hours of coding, an hour of code review on someone else's diff, and an hour of behavioral, all back to back with different interviewers. That structure is not an accident. Citadel's own engineering team describes the process as a series of 45 to 60 minute live sessions built to see whether a candidate can write correct, efficient code under time pressure, not just talk about how they'd approach a problem eventually. The firm's published guide to its process states plainly that the loop favors candidates who can move from a described problem to working code quickly, over candidates who explore several approaches out loud without committing to one (Citadel Careers, Engineering Interview Process).
The reputation for difficulty is not just internet folklore. Software Engineer candidates on Glassdoor rate the Citadel interview a 3.4 out of 5 for difficulty, and only 36 percent describe the overall experience as positive, which is low relative to most large tech employers (Glassdoor, Citadel Software Engineer Interviews). Quantitative research candidates report a similar pattern: a four-step process spanning four to five weeks, with 45 to 60 minute remote sessions on programming, data structures and algorithms, and past research, using a shared CoderPad link sent ahead of each call (Citadel Careers, Quantitative Research Interview Process).
This page covers Citadel interview questions across the rounds candidates actually report: the recruiter screen and culture questions, the online assessment, live coding and data structures rounds, system design questions built around trading infrastructure, C++ and concurrency questions specific to the engineering org, quantitative and markets domain questions, and the behavioral round Citadel structures around three themes its interviewers return to again and again: ownership, resilience under pressure, and intellectual honesty about your own mistakes.
Recruiter screen and "why Citadel"
The first call is short, usually 15 to 30 minutes, and it is not a technical filter so much as a fit check. Recruiters ask about your background, why you're looking, and whether you actually understand what the firm does day to day, which trips people up more than it should.
Easy questions
12Generic answers about prestige or compensation get filtered out immediately because every candidate says some version of that. A better answer names something concrete about how Citadel actually operates: the pace of feedback, the proximity between engineers and the people trading on what they build, or the density of hard technical problems relative to headcount. Then tie that back to your own history. If you've thrived when your work gets evaluated fast and directly rather than through layers of process, say so and back it with a specific example rather than an adjective.
What doesn't work is reciting the firm's AUM or founding year back at the recruiter. They already know that. They're listening for whether you've thought about what the day to day is actually like and whether you'd hold up in it.
This sounds like small talk but it's the recruiter building a narrative they'll repeat to the hiring team later. Keep each transition to one clear reason, not a list of reasons, and make sure the reasons form a coherent story rather than a series of unrelated events. If you left a job because the scope shrank, say that plainly instead of dressing it up.
Avoid negativity about past employers even if it's justified. Recruiters at trading firms hear a lot of pitches and remember the ones that sound bitter, and it costs you nothing to frame a bad situation factually instead of critically.
The naive approach sorts the array or uses a hash set to track seen values, both O(n) space at minimum for the hash approach or O(n log n) time for sorting. The optimal approach uses the sum and sum-of-squares identities: the expected sum of 1 to N minus the actual sum gives you the difference between the missing and duplicate values, and the same trick with squares gives you a second equation. Solving the two equations together isolates both numbers in O(n) time and O(1) extra space.
def find_missing_and_duplicate(nums, n):
expected_sum = n * (n + 1) // 2
expected_sq_sum = n * (n + 1) * (2 * n + 1) // 6
actual_sum = sum(nums)
actual_sq_sum = sum(x * x for x in nums)
diff = expected_sum - actual_sum # missing - duplicate
sq_diff = expected_sq_sum - actual_sq_sum # missing^2 - duplicate^2
total = sq_diff // diff # missing + duplicate
missing = (diff + total) // 2
duplicate = missing - diff
return missing, duplicateInterviewers expect you to name the naive solution first, then walk to this one and explain the tradeoff. Jumping straight to the equations without showing you understand the simpler version reads as memorized rather than reasoned.
Read all problems before writing any code. Candidates report the assessment tends to order problems roughly by difficulty but not always, and spending ten minutes upfront ranking them by your own confidence beats losing thirty minutes stuck on problem one while an easier problem three goes untouched.
Since near-full test case coverage is what separates a pass from a fail, budget time to actually run through edge cases, empty input, single element, all duplicates, negative numbers, rather than submitting the first version that compiles. A solution that handles the obvious case but fails three hidden edge cases scores worse than a slightly less elegant solution that passes everything.
The brute force approach concatenates everything and sorts once, giving O(n log n) where n is the total element count across all arrays. The better approach uses a min-heap seeded with the first element of each array. Pop the minimum, push the next element from whichever array it came from, and repeat until the heap empties. That gives O(n log k), which matters when k is large relative to array size.
import heapq
def merge_k_sorted(arrays):
heap = []
for i, arr in enumerate(arrays):
if arr:
heapq.heappush(heap, (arr[0], i, 0))
result = []
while heap:
val, arr_idx, elem_idx = heapq.heappop(heap)
result.append(val)
if elem_idx + 1 < len(arrays[arr_idx]):
next_val = arrays[arr_idx][elem_idx + 1]
heapq.heappush(heap, (next_val, arr_idx, elem_idx + 1))
return resultCandidates report a variant of this where arrays are streamed from a file reader rather than held in memory, which pushes the question toward a discussion of buffering and when to pull the next chunk from disk rather than just the pure algorithm.
Stack problems tend to describe nested or ordered matching, valid parentheses, evaluating expressions, finding the next greater element, anything where the most recently seen unmatched item needs to resolve before older ones. BFS problems describe shortest paths, level-by-level exploration, or minimum number of steps in an unweighted graph or grid.
The tell that catches people off guard under time pressure is a problem that looks like a stack problem on the surface but is actually asking for a minimum distance, which needs BFS instead. Reading the actual question being asked, minimum steps versus valid ordering, before picking a data structure saves you from writing a stack solution to a shortest-path problem and having to start over.
A stack handles this directly: push opening brackets, and on a closing bracket, check the top of the stack matches the corresponding opener. If it doesn't match, or the stack is empty when you hit a closer, the string is invalid. At the end, the stack must be empty for the string to be balanced.
def is_balanced(s):
pairs = {')': '(', ']': '[', '}': '{'}
stack = []
for ch in s:
if ch in '([{':
stack.append(ch)
elif ch in pairs:
if not stack or stack.pop() != pairs[ch]:
return False
return not stackThis one shows up as a warm-up more than as the differentiator, but candidates still lose points by not handling the empty-stack case on a closing bracket, which crashes instead of returning False if you pop without checking first.
A raw pointer has no ownership semantics at all. It doesn't know who's responsible for freeing the memory it points to, which is exactly how use-after-free and double-free bugs happen. unique_ptr expresses exclusive ownership: exactly one owner at a time, automatically freed when that owner goes out of scope, and it can be moved but not copied, which the compiler enforces.
std::unique_ptr<Order> order = std::make_unique<Order>(id, price);
// order.get() to peek at the raw pointer without transferring ownership
// std::move(order) to transfer ownership elsewhere
std::shared_ptr<Order> shared_order = std::make_shared<Order>(id, price);
auto copy = shared_order; // both now share ownership, refcount is 2shared_ptr uses reference counting to allow multiple owners, freeing the memory only when the last owner releases it, at the cost of the refcount's overhead and the risk of a reference cycle leaking memory if two shared_ptrs point at each other. In practice, unique_ptr should be the default and shared_ptr reserved for genuine shared-ownership cases, not used everywhere out of caution.
A market order says execute immediately at whatever price is currently available, prioritizing certainty of execution over price. A limit order specifies the worst price you're willing to accept and may not execute at all if the market never reaches that price, prioritizing price control over certainty.
A matching engine has to treat them structurally differently: a market order gets matched immediately against the best available price on the opposite side of the book with no price check, while a limit order gets inserted into the book at its specified price if it can't match immediately, and sits there as resting liquidity until a matching order arrives or it's canceled. Getting this distinction wrong in a system design answer, treating both as "just an order," is a fast way to lose credibility with an interviewer from a trading background.
The bid-ask spread is the gap between the highest price a buyer is currently willing to pay and the lowest price a seller is currently willing to accept. A narrow spread generally signals a liquid, actively traded instrument where buyers and sellers largely agree on price. A widening spread usually signals reduced liquidity, increased uncertainty, or higher perceived risk, since market makers demand more compensation for taking on inventory risk when conditions get less predictable.
This matters practically for anything you're asked to design that touches execution, since a system that assumes a tight, stable spread will make bad decisions exactly when conditions get volatile, which is also when correctness matters most.
Latency arbitrage is profiting from being faster than other participants to act on the same publicly available information, seeing a price move on one venue and trading on another venue before its price has updated to reflect that move. The opportunity exists only for the brief window before the slower venue's price catches up, which in liquid markets can be measured in microseconds rather than seconds.
That's the direct reason infrastructure at a firm like Citadel gets scrutinized down to the level of network hops, cache-line layout, and system call overhead, since a strategy that's theoretically correct but arrives even a few microseconds late captures none of the opportunity it was designed around. It's also why the system design questions in these interviews weight latency and determinism far more heavily than the horizontal-scalability concerns a typical consumer tech interview emphasizes.
A strong answer walks through five distinct parts: what you actually decided and why it seemed right at the time, the specific moment you realized it was wrong, who you told and how quickly, what you did immediately to limit the damage, and what changed afterward so the same mistake couldn't recur silently. Skipping straight from "I made a mistake" to "and I learned from it" without the middle steps reads as rehearsed rather than lived through.
The moment you discovered the problem matters more than interviewers let on. Candidates who found the mistake themselves, before someone else flagged it, tend to score better than candidates whose mistake was caught by someone else, since that's a direct signal about whether you're actively monitoring your own work or waiting to be told something's wrong.
Medium questions
21Citadel's engineering org supports specific business units, and candidates who've done basic homework know that a technology role sits closer to a particular strategy or infrastructure team than to some generic engineering pool. Being able to name the difference between building trading infrastructure, research tooling, or enterprise systems, and having a rough sense of which one you're interviewing for, signals you've actually researched the role rather than applying to "big quant firm" as a category.
You don't need inside knowledge here. You need to be able to say, in your own words, what the team you're interviewing for is likely responsible for, and why that specific work interests you more than an adjacent team's would.
This gets asked directly sometimes, and it's also being tested implicitly in every technical round that follows. The honest answer is that you treat it as new information, not as an attack. Restate what you now understand differently, adjust the approach, and keep working rather than getting defensive or shutting down.
Interviewers at Citadel deliberately push back on working solutions to see how a candidate reacts, since the firm's own hiring philosophy prizes disciplined problem solving under pressure over a smooth, uninterrupted demo. If your reaction to pushback is to freeze or argue, that tells them more than a correct first answer would.
Trading firms hire engineers from every kind of background and expect them to pick up market mechanics fast, so this question checks whether you can actually absorb a new domain rather than just memorize syntax. Pick an example with a real timeline: what you didn't know on day one, what forced you to learn it quickly, and what you could do by the point it mattered.
Avoid an answer where the "new domain" was really just a new framework. Interviewers are listening for evidence you can go from zero context to functional understanding of a business problem, not just a new API surface.
The naive approach recomputes frequencies for the whole window every time it slides, which is expensive if the window is large and slides often. A better structure keeps a frequency map alongside a way to quickly retrieve the top K, either a heap that tolerates stale entries with lazy deletion, or a bucket array indexed by frequency count when frequencies are bounded and small.
As the window slides, decrement the frequency of the element leaving and increment the frequency of the element entering, updating whichever structure you chose incrementally rather than rebuilding it. The interviewer is usually probing whether you default to recomputation out of habit or actually reason about what changes between one window position and the next.
Depth-first search with a coloring scheme handles this cleanly: mark a node gray when you enter it, black when you finish exploring all its neighbors, and white if unvisited. If DFS reaches a gray node, that's a back edge and confirms a cycle. To reconstruct the actual cycle, keep a parent pointer or path stack while traversing and walk it back from the node that closed the loop.
def find_cycle(graph):
WHITE, GRAY, BLACK = 0, 1, 2
color = {node: WHITE for node in graph}
path = []
def dfs(node):
color[node] = GRAY
path.append(node)
for neighbor in graph[node]:
if color[neighbor] == GRAY:
cycle_start = path.index(neighbor)
return path[cycle_start:]
if color[neighbor] == WHITE:
result = dfs(neighbor)
if result:
return result
color[node] = BLACK
path.pop()
return None
for node in graph:
if color[node] == WHITE:
result = dfs(node)
if result:
return result
return NoneThe single-transaction version tracks a running minimum and the best profit seen so far in one pass. The two-transaction version needs state for both transactions simultaneously: track the best profit if you've completed zero, one, or two buy-sell pairs by each point in the array, updating all four values on each day.
def max_profit_two_transactions(prices):
buy1 = buy2 = float('-inf')
sell1 = sell2 = 0
for price in prices:
buy1 = max(buy1, -price)
sell1 = max(sell1, buy1 + price)
buy2 = max(buy2, sell1 - price)
sell2 = max(sell2, buy2 + price)
return sell2Given the domain, expect the interviewer to push this further into a k-transaction general case, which needs a 2D DP table indexed by transaction count and day. Being able to see the pattern from two transactions to k transactions, rather than treating each as a separate memorized problem, is what they're actually testing.
The brute force computes, for each index, the max height to its left and the max height to its right, and the trapped water at that index is the smaller of those two maxes minus the current height, if positive. That's O(n) time but with repeated work if you recompute the maxes naively.
The two-pointer version drops extra space by tracking a running left-max and right-max, moving whichever pointer points at the smaller max inward, since the water level at that side is bounded by the smaller of the two maxes regardless of what's further along on the other side. That gets you O(n) time and O(1) space instead of the O(n) space a precomputed max-arrays version needs.
Breadth-first search from the start cell guarantees the shortest path in an unweighted grid, since BFS explores in order of increasing distance. Track visited cells to avoid re-processing, and terminate as soon as you dequeue a cell marked as an exit, since BFS guarantees that's the shortest path to any exit by construction.
from collections import deque
def shortest_path_to_exit(grid, start):
rows, cols = len(grid), len(grid[0])
visited = {start}
queue = deque([(start, 0)])
while queue:
(r, c), dist = queue.popleft()
if grid[r][c] == 'E':
return dist
for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and (nr, nc) not in visited and grid[nr][nc] != '#':
visited.add((nr, nc))
queue.append(((nr, nc), dist + 1))
return -1If an approximate count is acceptable, a HyperLogLog or similar probabilistic counter gets you a distinct count in fixed memory regardless of input size, trading a small, bounded error rate for a massive memory reduction. If an exact count is required, external sorting is the standard approach: sort chunks that fit in memory, write them to disk, then merge-count duplicates across the sorted runs, which turns an unbounded memory problem into a bounded disk I/O problem.
The interviewer is checking whether you reach for "just load it into a hash set" as a reflex, notice it doesn't fit the stated constraint, and pivot to a real external-memory technique instead of hand-waving past the constraint.
A hash map alone gives O(1) lookup but no way to track recency cheaply. A doubly linked list alone tracks recency and supports O(1) removal from the middle, but lookup by key is O(n). Combining both, a hash map from key to linked list node, plus the doubly linked list ordered by recency, gets O(1) for both operations: the map finds the node instantly, and the list lets you move it to the front or evict from the back in constant time.
class Node:
def __init__(self, key, val):
self.key, self.val = key, val
self.prev = self.next = None
class LRUCache:
def __init__(self, capacity):
self.cap = capacity
self.map = {}
self.head = Node(0, 0)
self.tail = Node(0, 0)
self.head.next = self.tail
self.tail.prev = self.head
def _remove(self, node):
node.prev.next, node.next.prev = node.next, node.prev
def _add_front(self, node):
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
def get(self, key):
if key not in self.map:
return -1
node = self.map[key]
self._remove(node)
self._add_front(node)
return node.val
def put(self, key, val):
if key in self.map:
self._remove(self.map[key])
node = Node(key, val)
self.map[key] = node
self._add_front(node)
if len(self.map) > self.cap:
lru = self.tail.prev
self._remove(lru)
del self.map[lru.key]This one recurs across trading-firm interviews specifically because caches sit everywhere in real market-data and order-routing systems, so a clean implementation here reads as directly relevant rather than academic.
The client generates a unique idempotency key per logical order attempt before the first send, and the server stores that key against the resulting order state, order accepted, order rejected, or in-flight, before doing anything else. A retry with the same key returns the already-recorded result instead of processing the order a second time.
The subtlety is the window between receiving a request and durably recording its key. If the server crashes after accepting an order but before persisting the idempotency record, a retry could still double-process. The fix is making the persistence of the idempotency key and the order acceptance a single atomic operation, not two separate steps, which is the detail that separates a working answer from one that looks right but has a race condition.
A race condition happens when two or more threads access shared data concurrently, at least one of them writes, and the final outcome depends on the specific timing of their execution rather than being deterministic. The classic example is two threads incrementing the same counter without synchronization.
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // read, add one, write back: three separate steps
}
}
// two threads calling increment() concurrently can lose updates,
// because thread A's read-modify-write can interleave with thread B'sThe fix is either a mutex around the critical section, or an atomic type that guarantees the read-modify-write happens as one indivisible operation. Interviewers usually follow up by asking which fix you'd pick and why, since a mutex protects arbitrary critical sections but costs more, while an atomic is cheaper but only works for genuinely simple operations like a counter.
A mutex, when contended, puts the waiting thread to sleep and lets the operating system schedule something else, which is efficient when the lock is likely to be held for a while, since the waiting thread isn't burning CPU cycles doing nothing. A spinlock keeps the waiting thread actively polling the lock in a tight loop instead of sleeping, which wastes CPU while waiting but avoids the cost of a context switch entirely.
Spinlocks make sense specifically when the critical section is very short and contention is expected to resolve quickly, exactly the profile of a low-latency trading path, where a context switch's cost can dwarf the actual work being protected. Using a spinlock for a critical section that might block on I/O or hold the lock for a long time is a common mistake, since it turns "waste a little CPU briefly" into "waste a lot of CPU for a long time."
A mutex protecting the queue's internal state, combined with a condition variable, handles this correctly. The producer locks the mutex, pushes an item, and notifies the condition variable. The consumer locks the mutex, waits on the condition variable while the queue is empty, and pops once notified, always rechecking the actual condition after waking rather than assuming the wakeup itself guarantees the queue is non-empty.
std::mutex m;
std::condition_variable cv;
std::queue<int> q;
void produce(int val) {
std::lock_guard<std::mutex> lock(m);
q.push(val);
cv.notify_one();
}
int consume() {
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, [] { return !q.empty(); }); // guards against spurious wakeups
int val = q.front();
q.pop();
return val;
}The predicate passed to wait is what protects against spurious wakeups, a real phenomenon on most platforms where a waiting thread can wake up without actually being notified, so checking the queue's real state after waking, rather than trusting the wakeup itself, is the detail that separates correct code from code that mostly works.
Compute two moving averages of different windows over the price series, and generate a buy or sell signal whenever the short-window average crosses the long-window average. The naive backtest just sums the return from each signal, but that overstates performance by ignoring the real cost of trading, bid-ask spread paid on entry and exit, and any explicit commission.
Subtracting a realistic transaction cost per trade from every signal's return is what separates a strategy that looks profitable on paper from one that's actually profitable after friction. A strategy that generates many small, frequent signals is far more sensitive to this adjustment than one that trades rarely, since transaction costs compound with trade frequency in a way that a naive backtest completely hides.
A stationary series has statistical properties, mean, variance, autocorrelation, that stay stable over time. Raw price data is almost never stationary, since prices trend and their variance changes over time, which means a model trained on one period's raw prices often fails on a later period simply because the underlying statistics shifted, not because the model itself was wrong.
Returns, the percentage change between consecutive prices, tend to be much closer to stationary than raw prices, which is why most quantitative models work on returns or log returns rather than raw price levels. Being able to explain this distinction, and why it changes what you'd actually feed into a model, is a common way interviewers check whether a candidate has done real quantitative work or just knows the vocabulary.
With enough assets and a long enough lookback window, some pairs will show a strong correlation purely by chance, especially if both series are trending rather than stationary, since two unrelated trending series can appear highly correlated without any actual relationship. A first check is whether the correlation holds up out of sample, on a different time period than the one it was discovered in, since a spurious correlation typically doesn't survive that test.
A more rigorous check looks for a plausible causal or economic mechanism behind the relationship rather than accepting a statistical correlation on its own, and tests whether the relationship is stable across different market regimes rather than only holding during one specific period. Interviewers use this to see whether a candidate treats a correlation number as the end of the analysis or the start of it.
The strongest answers quantify the actual constraint rather than describing pressure in the abstract, hours before a deadline, a specific data quality problem discovered close to a critical window, a system degrading with a hard cutoff approaching. Vague pressure ("it was really stressful") doesn't give an interviewer anything to evaluate.
Explain the actual triage you did, what you decided to fix first and why, what you consciously chose to leave for later, and what information you had versus what you were guessing at when you made the call. Interviewers are listening for evidence of a real decision process under constraint, not for evidence that you stayed calm, which on its own tells them very little about how you'd actually perform.
Citadel's behavioral round pushes hard for concrete metrics rather than qualitative claims, a latency reduction in milliseconds, a specific error rate before and after a fix, a throughput number that changed after an optimization. An answer that stays at "it made things faster and more reliable" without a number attached reads as either exaggerated or poorly measured, and interviewers here notice the hedge.
If you genuinely don't have a precise number for something, say that directly rather than inventing one, and give the closest honest approximation you do have. Intellectual honesty about the limits of your own data is itself part of what's being evaluated, not a weakness to paper over with a made-up statistic.
Describe the actual substance of the disagreement, not just that one existed, since the specifics reveal whether you engaged with the technical merits or just deferred to seniority or dug in stubbornly. What matters most here is whether the resolution came from evidence, a benchmark, a prototype, actual data, rather than from whoever argued more persuasively or had more authority.
If the disagreement resolved in your favor, be honest about what you'd have done if the evidence had gone the other way. If it resolved against you, describe what you actually did afterward, whether you executed the decision fully or quietly under-invested in something you disagreed with, since the second option is a bigger red flag than most candidates realize.
The upward communication piece is specifically what interviewers probe for here, not just that a problem existed and got fixed. Walk through exactly what you said, how quickly after discovering the problem you said it, and what you included versus left out given the time constraint. A candidate who sat on bad news even briefly to "gather more information first" often gets pushed on why, since in a fast-moving environment, that delay itself can be the more costly mistake.
Being specific about the actual words or framing you used, rather than describing the conversation abstractly, makes this answer land better, since it's evidence you've actually done this rather than reasoned about how you'd do it in theory.
Hard questions
7A min-heap of size k processes the data in a single streaming pass: push each element, and once the heap exceeds size k, pop the minimum. Whatever remains at the top when the stream ends is the kth largest, using O(k) memory regardless of the total dataset size.
If k itself is large enough that even O(k) memory is a problem, the quickselect-style approach with external chunking becomes necessary: partition the data into memory-sized chunks, find a rough pivot from a sample, and use multiple passes to progressively narrow down which chunk contains the kth largest value. Interviewers use the follow-up, what if k is close to the size of the entire dataset, to see whether a candidate defaults to the heap answer without checking it still fits the stated memory constraint.
A single valid order is a straightforward topological sort, either Kahn's algorithm using in-degree counting and a queue, or DFS with a finish-time stack. Enumerating all valid orders is a different, more expensive problem: backtrack over the set of nodes with zero remaining in-degree at each step, trying each as the next task, decrementing the in-degree of its dependents, recursing, then restoring state before trying the next candidate.
Interviewers watch for whether a candidate immediately recognizes that "all orders" implies exponential output in the worst case, and reasons about that upfront, rather than writing a single-topological-sort solution and only discovering the mismatch when asked to produce a second valid ordering.
A naive sum-per-query is O(n) per query, which gets expensive fast if queries are frequent. A prefix sum array gets queries down to O(1), but rebuilding the prefix array after every update costs O(n) again, just moving the cost from query to update. A Fenwick tree, also called a binary indexed tree, gets both operations down to O(log n): updates propagate through O(log n) nodes, and range sum queries are computed from O(log n) partial sums.
class FenwickTree:
def __init__(self, n):
self.tree = [0] * (n + 1)
def update(self, i, delta):
i += 1
while i < len(self.tree):
self.tree[i] += delta
i += i & (-i)
def prefix_sum(self, i):
i += 1
total = 0
while i > 0:
total += self.tree[i]
i -= i & (-i)
return total
def range_sum(self, l, r):
return self.prefix_sum(r) - (self.prefix_sum(l - 1) if l > 0 else 0)Trading-adjacent interviews come back to this pattern often because it's exactly the shape of a running P&L or position-tracking query against a fast-updating book. Being fluent with the Fenwick tree rather than reconstructing it from scratch under time pressure is worth the memorization.
A single-venue matching engine can assume it has the full, current view of the book at all times. Cross-venue routing means acting on a view that's necessarily stale by however long it takes market data to travel from each venue, which changes the entire correctness model from "always accurate" to "accurate within a known bound, with explicit handling for when that bound is violated."
Practically, that means the router needs a per-venue staleness estimate and has to decide, per order, whether to route based on possibly-stale data or wait for a fresher update, a tradeoff between speed and correctness that doesn't exist in the single-venue version. Candidates who bring up this staleness problem unprompted, rather than needing it pointed out, tend to score noticeably better on this follow-up.
False sharing happens when two threads modify different variables that happen to sit on the same CPU cache line, so even though the variables themselves aren't logically related, the cache coherency protocol invalidates the entire line every time either thread writes, forcing both cores to keep re-fetching the line from a shared cache level or memory. The threads aren't actually contending for the same data, but the hardware behaves as if they are.
struct Counters {
std::atomic<int> a; // written by thread 1
std::atomic<int> b; // written by thread 2, likely same cache line as a
};
// fix: pad or align so a and b land on separate cache lines
struct alignas(64) PaddedCounters {
std::atomic<int> a;
char padding[60];
std::atomic<int> b;
};Detecting it usually means profiling with hardware performance counters looking for unusually high cache-line invalidation traffic relative to the actual logical contention in the code, since false sharing doesn't show up as a correctness bug, it shows up purely as unexplained latency. This is exactly the kind of bug that's invisible in a single-threaded review and only surfaces under real multithreaded load, which is why it's a favorite question at firms whose systems live or die on microseconds.
Every object of a class with at least one virtual function carries a hidden pointer, the vptr, to a vtable, a static array of function pointers shared by all instances of that class. Calling a virtual function means dereferencing the vptr to find the vtable, then indexing into it to find the actual function address, then jumping to that address, an extra layer of indirection compared to a regular function call, which the compiler can resolve directly at compile time.
That indirection also defeats certain compiler optimizations, inlining in particular, since the compiler often can't know at compile time which override will actually run at a given call site. In a latency-sensitive hot path, this is a real reason engineers sometimes avoid virtual dispatch in favor of templates or explicit branching, even though it costs some code clarity in exchange for a few nanoseconds that matter at scale.
This is less a behavioral question than a stress test of whether the story on your resume is actually yours. Interviewers here follow up aggressively on details, why you chose that specific approach over alternatives, what the actual failure mode was before your fix, what you'd do differently now with more context. A candidate who can't go two or three layers deep into their own claimed accomplishment loses credibility fast, and that credibility loss tends to color how the rest of the interview gets read.
Pick a story you genuinely understand at the level of tradeoffs and failure modes, not just the level of "here's what I built," since the follow-up questions are specifically designed to find the edge of your actual understanding rather than the edge of your prepared narrative.
Real-time scenario questions
9Maintain two heaps: a max-heap holding the smaller half of numbers seen so far, and a min-heap holding the larger half. Keep them balanced within one element of each other. On insert, add the new number to the appropriate heap based on comparison with the max-heap's top, then rebalance if one heap grows more than one element larger than the other. The median is either the top of the larger heap, or the average of both tops when they're equal in size.
import heapq
class MedianFinder:
def __init__(self):
self.small = [] # max-heap, stored as negatives
self.large = [] # min-heap
def add_num(self, num):
heapq.heappush(self.small, -num)
heapq.heappush(self.large, -heapq.heappop(self.small))
if len(self.large) > len(self.small):
heapq.heappush(self.small, -heapq.heappop(self.large))
def find_median(self):
if len(self.small) > len(self.large):
return -self.small[0]
return (-self.small[0] + self.large[0]) / 2.0This shows up on Citadel's most-asked list frequently enough that it's worth being able to derive without hesitation, not just recite. The follow-up question is almost always what happens with duplicate values or a very large stream, and whether you'd change the structure for memory reasons.
Price-time priority means orders at a better price always execute first, and among orders at the same price, the earliest one submitted executes first. That maps naturally onto two levels of structure: a sorted structure keyed by price, a red-black tree or skip list for bids and another for asks, and within each price level, a FIFO queue of orders in arrival order.
Matching walks the best bid against the best ask whenever they cross, filling from the front of each price level's queue first. Cancellation needs O(log n) lookup to the right price level plus O(1) removal from that level's queue if you keep a direct reference from order ID to its queue node, which argues for a hash map from order ID to node alongside the price-level structure, the same combined-structure pattern as an LRU cache but applied to price levels instead of recency.
Full snapshots are expensive to send on every tick but cheap to reason about, incremental updates are cheap to send but compound if a consumer misses one. The standard pattern combines both: publish periodic full snapshots plus a stream of incremental diffs between them, tagged with sequence numbers, so a consumer that falls behind can detect the gap from a missing sequence number and request a fresh snapshot rather than silently drifting out of sync.
Bounded skew across consumers usually means multicast or a fan-out broadcast rather than point-to-point delivery, since point-to-point delivery to many consumers means the last consumer in line gets data measurably later than the first. The interviewer is listening for whether you reach for sequence numbers and gap detection unprompted, since that's the mechanism that actually prevents silent data corruption downstream.
The core requirement most candidates miss is that a backtest has to replay historical data in the exact order and timing it originally arrived, including the latency between when data became available and when a real system could have acted on it. A backtest that lets a strategy see the full day's data instantly and act on all of it at once produces numbers that look great and mean nothing, because no real system could have traded that way.
Architecturally, that means an event-driven replay engine, feeding market events through the same interface the live system consumes, with the strategy code under test genuinely unaware whether it's running against live data or replayed history. Keeping that boundary strict is what prevents lookahead bias from creeping into results, and it's usually the exact point an interviewer probes to see if you actually understand why backtests are so often wrong in practice.
Every fill needs to update position and realized P&L atomically, since a fill that updates position but crashes before updating P&L, or vice versa, leaves the system in a state that's wrong in a way that's hard to detect later. The natural approach processes fills through a single ordered log per instrument or account, applying updates sequentially rather than allowing concurrent writers to race on the same position.
Mark-to-market P&L, the unrealized component based on current market price, needs to be recomputed continuously as prices move, which argues for separating it from realized P&L, updated only on fills, and unrealized P&L, updated on both fills and price ticks, since they have very different update frequencies and consistency requirements.
Preorder traversal with explicit null markers handles this cleanly and reversibly. Serialize by writing the current node's value, then recursively serializing the left subtree, then the right subtree, writing a sentinel for every null child. Deserialize by reading tokens in the same order they were written, consuming one token per recursive call, which naturally reconstructs the tree because preorder always visits a parent before its children.
def serialize(root):
vals = []
def dfs(node):
if not node:
vals.append('#')
return
vals.append(str(node.val))
dfs(node.left)
dfs(node.right)
dfs(root)
return ','.join(vals)
def deserialize(data):
vals = iter(data.split(','))
def build():
val = next(vals)
if val == '#':
return None
node = TreeNode(int(val))
node.left = build()
node.right = build()
return node
return build()The follow-up worth preparing for is doing the same thing for an arbitrary graph rather than a tree, where cycles mean you need a visited-node map during both serialization and reconstruction to avoid infinite recursion.
The tension here is that a correct risk check, checking credit limits, position limits, and price bands against current state, takes real computation, but the whole point of the service is to not add meaningful latency to order entry. The usual answer keeps risk state, current position, remaining credit, in memory local to the check itself rather than fetching it from a separate service on every order, updating that local state asynchronously as fills and cancels come in.
Where it gets hard is staleness: local state updated asynchronously can be a few orders behind the true position, and the interviewer will usually push on how much staleness is acceptable before the risk check itself becomes the risk. A reasonable answer sets a hard bound, reject conservatively if local state hasn't been refreshed within some strict window, rather than assuming eventual consistency is good enough for a check whose entire purpose is preventing catastrophic exposure.
The instinct to route every alert through a general-purpose message queue or a full logging pipeline usually adds exactly the latency the system is meant to avoid. A design built for milliseconds needs the anomaly detection and the alert dispatch living close together, ideally in the same process or on the same host, with the notification fired directly rather than passed through several hops of infrastructure that were built for durability over speed.
Durability still matters, you don't want to silently drop an alert because the fast path failed, so a reasonable design fires the low-latency path first and asynchronously logs to a durable store afterward for auditing, rather than gating the alert on the durable write completing. The interviewer is checking whether a candidate defaults to "put it on a queue" as a reflex without noticing that the queue itself is the latency source in this specific case.
Recovery correctness depends entirely on what was durably persisted before the crash. If every state-changing event, order accepted, order filled, order canceled, was written to a durable, ordered log before being acknowledged to the client, recovery is a matter of replaying that log from the last known-good checkpoint to reconstruct current state exactly.
The harder part is reconciling with the outside world during replay. Orders that were sent to an exchange but whose acknowledgment was lost in the crash need a reconciliation step against the exchange's own record of open orders, not just a replay of your own log, since your own log can't tell you what actually happened on the other side of a connection that dropped mid-flight. This is usually the exact detail interviewers are listening for, since a candidate who only replays their own log and never mentions reconciling against the external system's state is missing the failure mode that actually causes incidents.
How to prepare for a Citadel interview
Skip the pass around utility algorithms in isolation and spend your prep time on speed under a hard time limit, since the reported OA bar is passing nearly every test case within roughly 75 minutes across two or three problems, not producing a technically correct but slow or partial solution. Time yourself on medium and hard problems specifically, since candidates consistently describe hard problems as a real prerequisite here rather than an occasional bonus question.
For the system design and behavioral rounds specifically, the preparation that actually transfers is different from generic system design prep. Build familiarity with the shape of exchange-style problems, order books, matching engines, market data fan-out, pre-trade risk, since interviewers here weight latency, correctness under concurrency, and recovery from partial failure far more heavily than the horizontal scalability story that dominates most consumer tech system design prep. For behavioral, write out two or three real stories in advance with actual numbers attached, since vague or unquantified answers are the single most common thing that trips candidates up in this round.
Get the reps in before the real thing
LastRoundAI's mock interview mode runs live coding and system design sessions with real-time follow-up questions in your browser, which matters here specifically because Citadel's loop is built around live pushback, not a take-home you can polish alone. The free plan includes 15 credits a month that reset monthly, and Starter is $29/mo (or $19/mo on the 3-month plan) if you want more sessions before a real loop.
Once you can hold up under a follow-up, Auto-Apply queues tailored applications for your review, 10 a month free, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.
Questions about either product go to contact@lastroundai.com.
How this list was built
Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.
What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.
If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.
Frequently asked questions
What behavioural questions come up at Citadel?
Ownership under pressure, handling a mistake with financial or client consequence, and working with people who disagree with you. Regulated environments care about judgement and escalation, so answers that show you knew when to raise something land well.
How should I prepare for the final round at Citadel?
Know the business line you are joining, not just the firm. Candidates who can name what the desk or team actually does, and ask a specific question about it, separate themselves quickly at this stage.
What does the Citadel interview process look like?
Typically a recruiter screen, one or two technical or case rounds, and a final round with senior staff. Technology roles add a coding assessment; front-office roles lean harder on market awareness and behavioural depth. Expect the bar on precision to be higher than at a typical product company.
How technical is the Citadel interview?
Technical enough that hand-waving fails. For engineering roles expect data structures, system design and questions about correctness under concurrency or scale. For analyst roles expect quantitative reasoning and comfort explaining a number you produced.
Does Citadel ask brainteasers or probability questions?
Quantitative roles frequently do; general technology roles less often than their reputation suggests. If the role touches trading or research, probability and expected-value reasoning are fair game and worth rehearsing out loud.
- Citadel Careers, Our Engineering Interview Process
- Citadel Careers, Our Quantitative Research Interview Process
- Glassdoor, Citadel Software Engineer Interview Questions
- Glassdoor, Citadel Quantitative Researcher Interview Questions
- LeetCode, Citadel Problem List
- Wall Street Oasis, Citadel Securities Interview Questions
