Algo Trading Developer Interview Questions
Algo Trading Developer Interview Questions and Answers
This page is for people interviewing for algorithmic trading roles on either side of the desk: quant researchers and developers who build alpha signals, and trading-systems engineers who build the market data, order handling and execution code those signals run on. Many teams interview both groups with an overlapping loop, so the bank covers both. If you have not yet read the role page, start with Algo Trading Developer for the skills map, then come back here for the questions themselves.
A typical loop has a probability and mental maths screen, one or two coding rounds (Python for research, C++ for systems), a statistics or research round where you critique a backtest, a market microstructure round, and a design round on either a research pipeline or a low-latency trading path. Interviewers look for precise reasoning under time pressure, honest handling of uncertainty, and a habit of asking how a result could be wrong before believing it.
The page is organised in the order most candidates should study: probability and statistics first, then alpha research and backtesting, then portfolio and risk, then microstructure, execution and FIX, and finally low-latency engineering. After the question sections come ten coding exercises with tested solutions, five open-ended scenarios with model walk-throughs, a mock interview plan, a cheat sheet, red flags and an FAQ.
- Questions
- 62 with model answers
- Coding
- 10 exercises with solutions
- Scenarios
- 5 design and debugging rounds
How to use this page
Read each question and say your answer out loud before you open the model answer. Time yourself: most answers should take one to two minutes.
Do the probability and mental maths questions without paper first, then check your working on paper. Speed and calm matter as much as the final number in a quant screen.
Write every coding exercise from a blank file, run it, and only then compare with the solution. Python solutions use only the standard library so you can practise anywhere. Use coding practice for more timed problems.
Work through each scenario on paper as if you were at a whiteboard, then compare your structure with the model walk-through rather than the exact words.
Finish with the mock interview plan, and use AI code review round to practise spotting bugs in other people's code, which is a common round in trading firms.
Probability, brainteasers and mental maths
Quant screens open with quick probability and arithmetic to see whether you reason cleanly and stay calm when you do not know the answer straight away.
How many times do you expect to roll a fair die until you see a six?Junior
Six. The number of rolls is geometric with success probability 1/6, and a geometric variable has mean 1/p.
A neat way to show it without the formula is a one-step recursion. Let E be the expected number of rolls. You always spend one roll; with probability 1/6 you stop, otherwise you are back where you started. So E = 1 + (5/6)E, which gives E/6 = 1 and E = 6.
Interviewers like the recursion because it generalises: the same trick handles expected tosses until two heads in a row, gambler's ruin and most stopping problems. Say the structure aloud (state, one step, condition on the outcome) and the arithmetic follows.
Likely follow-up. What is the expected number of rolls to see every face at least once?
What is the expected number of coin tosses until you get two heads in a row, and how does it compare with heads then tails?Junior
Two heads in a row takes 6 tosses on average; heads then tails takes 4.
For HH, define states by progress: E0 from scratch and E1 after one head. E1 = 1 + (1/2)(0) + (1/2)E0, because a head finishes and a tail sends you back to the start. E0 = 1 + (1/2)E1 + (1/2)E0. Solving gives E0 = 6. For HT, once you have a head you never lose progress: a further head still leaves you one step away. So E1 = 2 and E0 = 2 + 2 = 4.
The point interviewers want is why they differ even though each pattern has probability 1/4 per pair: HH can overlap with itself, so failures throw away progress. This self-overlap idea appears again in pattern-matching games and in Penney's game.
What is the probability of at least one six in four rolls of a fair die?Junior
About 0.518. Use the complement: the chance of no six in four rolls is (5/6) to the fourth power, which is 625/1296, so the answer is 671/1296.
For mental arithmetic, 625/1296 is a little under 0.5 because 1296 is just over twice 625 (1250). So the answer is a little over a half. Getting from 0.48 to 0.518 quickly is what the interviewer is checking, not long division.
The complement trick is the default whenever a question says at least one. A common mistake is to add 4 times 1/6 and get 2/3, which ignores overlaps and breaks as soon as the number of rolls grows past six.
Likely follow-up. How many rolls do you need for the chance of at least one six to pass 90%?
Make me a market on the sum of two dice. What do you quote and why?Mid
I centre the quote on the expected value, 7, and set the width from the uncertainty and my risk appetite. The variance of one die is 35/12, so the sum has variance 35/6, roughly 5.83, and a standard deviation of about 2.4.
A sensible opening is something like 6.5 at 7.5: tight enough to be useful, wide enough that a single informed trader does not hurt me badly. If the interviewer buys at 7.5 and keeps buying, I ask myself whether they know something, for example that they will choose which dice count, and I move my quote up. If they reveal information (one die shows a 5), I update: the expectation becomes 5 + 3.5 = 8.5 and the variance halves.
What is being tested is the trading loop: estimate fair value, quote around it, learn from the flow, and manage inventory. Saying nothing until you are sure is the wrong behaviour in this game.
Likely follow-up. You are long ten contracts after buying at 7.5. Do you shade your bid or your offer?
A test for a condition is 99% accurate and 1 in 1,000 people have the condition. If someone tests positive, what is the chance they have it?Mid
About 9%, assuming 99% means both a 99% true positive rate and a 1% false positive rate.
Use counts, which is faster and less error-prone than the formula. Out of 100,000 people, 100 have the condition and 99 of them test positive. Of the 99,900 without it, 1% test positive, which is 999. So among 1,098 positives only 99 are true, a little over 9%.
The trading version of this is a signal that fires rarely. A detector of informed flow that is right 99% of the time can still produce mostly false alarms if informed flow is rare. The base rate dominates, and that is why research teams always ask what the unconditional frequency of the event is before they get excited about a classifier's accuracy.
Two random variables each have correlation 0.9 with a third. What is the lowest possible correlation between the two?Mid
About 0.62. Correlations behave like cosines of angles between vectors, so the question is how far apart two vectors can be if each is within the angle whose cosine is 0.9 of a third.
The bound is rho_AC at least rho_AB times rho_BC minus the square root of (1 minus rho_AB squared) times (1 minus rho_BC squared). With 0.9 and 0.9 that is 0.81 minus 0.19, which is 0.62. It comes from requiring the 3 by 3 correlation matrix to be positive semi-definite.
The practical lesson: correlation is not transitive in a loose sense. Two signals that are each highly correlated with a benchmark can still differ meaningfully from each other, and when you build a correlation matrix by hand (for stress tests, say) you must check it is positive semi-definite or your risk model will return negative variances.
Likely follow-up. What happens to the lower bound if both correlations are 0.5?
You can bet a fraction of your wealth repeatedly on a coin that lands heads 60% of the time, at even money. What fraction do you bet?Mid
The Kelly fraction is 20%: f = p minus q divided by the odds, which is 0.6 minus 0.4 over 1. Kelly maximises the expected logarithm of wealth, which maximises long-run growth.
I would say the answer and then qualify it. Full Kelly has large drawdowns and assumes you know p exactly. If your estimate of the edge is noisy, betting full Kelly on an overestimate cuts growth sharply, and betting twice Kelly gives zero expected growth. Practitioners usually bet a fraction of Kelly, often half, which gives up a little growth for much lower volatility.
In portfolio terms, Kelly sizing for a single strategy is roughly expected excess return divided by variance. That links directly to mean-variance sizing, and is a good bridge into the portfolio questions that usually follow.
Estimate the square root of 252, and explain why the number matters.Junior
About 15.9. Since 16 squared is 256, the root of 252 is a little less than 16; a first-order correction gives 16 minus 4/32, which is 15.875.
It matters because 252 is the usual count of trading days in a year, and under independent returns volatility scales with the square root of time. A daily volatility of 1% is about 16% annualised, and a daily Sharpe ratio of 0.1 is about 1.6 annualised. Interviewers expect you to move between daily and annual figures in your head.
Mention the caveat: the square-root rule assumes returns are uncorrelated over time. Trending or mean-reverting returns break it, which is one reason to estimate volatility at the horizon you actually trade.
A fair coin random walk starts at 1. It stops at 0 or at N. What is the probability it reaches N, and how long does it take on average?Senior
The probability of reaching N is 1/N, and the expected duration is N minus 1 steps.
For the probability, a fair walk is a martingale, so its expected value at the stopping time equals its starting value. The stopped value is N with probability p and 0 otherwise, so pN = 1. For the duration, use the martingale X squared minus t: its expectation at stopping equals 1, so pN squared minus E[T] = 1, giving E[T] = N minus 1. In general, starting at k, the duration is k times (N minus k).
This is the gambler's ruin problem. In trading it is a reminder that a strategy with zero edge and a finite bankroll eventually ruins you against a much larger counterparty, and the optional stopping argument is the tool interviewers want to see.
Likely follow-up. How does the probability change if the coin is biased?
Statistics and hypothesis testing for research
Research interviews test whether you can tell a real effect from noise, and whether you know which standard statistical assumptions financial data breaks.
What is a p-value, and what does it not tell you?Junior
A p-value is the probability, assuming the null hypothesis is true, of seeing a test statistic at least as extreme as the one observed. A small value means the data would be surprising under the null.
It does not tell you the probability that the null is true, it does not measure the size of the effect, and it says nothing about whether the effect is economically useful after costs. A strategy can have a tiny p-value and a return that disappears once you pay the spread.
In research the biggest trap is that p-values assume you ran one pre-planned test. If you tried two hundred signal variants and report the best, the nominal p-value is far too optimistic. I would mention multiple testing straight away, because that is where the interviewer is heading.
A strategy has an annualised Sharpe ratio of 1.0 over four years of daily data. Is that statistically significant?Mid
Only marginally. Under independent, roughly normal returns, the t-statistic of the mean return is about the annualised Sharpe ratio times the square root of the number of years, so 1.0 times 2 gives a t of about 2.
That is borderline at the usual 5% level for a single test, and weak if this strategy was one of many tried. Fat tails, autocorrelation and regime changes widen the real uncertainty further. A rough standard error for an estimated annual Sharpe is about 1 over the square root of the years observed, so four years gives a confidence band of roughly plus or minus 1.
I would say four years of daily data sounds like a lot of observations, but for the mean it is the calendar span that matters, not the number of samples. That is why short-horizon strategies with many independent bets reach significance faster than monthly rebalanced ones.
Likely follow-up. How long a track record would you need to be confident that a Sharpe of 0.5 is positive?
How do you deal with testing many signals at once?Mid
I control for multiple comparisons and, more importantly, keep the testing process honest.
On the statistical side, Bonferroni divides the significance level by the number of tests and controls the chance of any false positive; it is simple but very conservative when tests are correlated. Holm's step-down method is uniformly better. For research, where I expect several true effects, controlling the false discovery rate with Benjamini and Hochberg is usually the better target. For Sharpe ratios specifically, you can deflate the best observed Sharpe for the number of trials and the variance across trials.
On the process side, I record every variant I try, keep a final hold-out period that nobody touches until the end, and prefer signals with an economic story so the hypothesis comes before the data. The count of trials is the input everyone forgets to log, and without it no correction is possible.
Why are overlapping returns a problem in regressions, and how do you fix the standard errors?Mid
If you regress 20-day forward returns on a daily signal, consecutive observations share 19 days of the same return, so the residuals are strongly autocorrelated. Ordinary least squares standard errors assume independent residuals and come out far too small, which makes t-statistics look much better than they are.
Fixes, in order of preference: sample non-overlapping observations (every 20th day) as a sanity check; use heteroscedasticity and autocorrelation consistent standard errors such as Newey and West with a lag window at least as long as the overlap; or bootstrap in blocks longer than the overlap so the dependence is preserved.
The point estimate of the slope is usually fine. It is the confidence you attach to it that is wrong. I would also check whether the effective number of independent observations is large enough to say anything at all.
Financial returns are fat-tailed. What does that change in practice?Mid
It means extreme moves are much more frequent than a normal distribution predicts, so any method that leans on normality underestimates risk and overstates confidence.
Concretely: value at risk from a normal model is too small in the tails, so I prefer historical or filtered historical simulation and look at expected shortfall too. Sample means and variances are dominated by a few days, so I report results with and without the largest days and use rank-based statistics such as Spearman correlation for signal evaluation. In regressions I winsorise or clip inputs at sensible levels so a single bad tick does not set the coefficient.
Volatility also clusters, so returns are closer to normal once scaled by a conditional volatility estimate. Scaling positions by recent volatility is one of the simplest and most useful responses to fat tails.
Likely follow-up. How would you test whether a return series is normal?
What is stationarity, and why does it matter for a pairs trading strategy?Senior
A series is weakly stationary if its mean, variance and autocovariances do not change over time. Prices are generally not stationary; returns are closer.
Pairs trading relies on a spread between two prices being stationary, so that deviations revert. Two non-stationary prices whose linear combination is stationary are cointegrated. The usual test is to estimate the hedge ratio by regression and test the residual for a unit root, or to use a Johansen test for more than two series. A high correlation of returns is not enough: two random walks can have correlated returns while their spread drifts without bound.
The practical risks are that the hedge ratio itself drifts, that the half-life of reversion is too long to trade after costs, and that the relationship breaks after a structural event such as a merger. I would estimate the half-life, check stability on rolling windows, and set a stop for when the spread exceeds anything seen in the fitting window.
How would you check that a regression of returns on several factors is giving you trustworthy coefficients?Senior
I check the assumptions that matter most for financial data and look at stability rather than a single fit.
First, multicollinearity: if factors are highly correlated, coefficients become unstable and can flip sign while the fit barely changes. I look at the correlation matrix and variance inflation factors, and consider orthogonalising or using ridge regression. Second, heteroscedasticity and autocorrelation, which bias the standard errors; I use standard errors that allow for both, or errors clustered by date in a panel. Third, outliers, which I handle by winsorising inputs.
Then stability: rolling or expanding window estimates, subperiod splits and out-of-sample predictive performance. A coefficient that only holds in one regime is a description of history rather than a model. Finally I check the economic size: a significant coefficient that implies a return smaller than trading costs is not useful.
Alpha research: signals and factor models
Interviewers want to see that you can define a signal precisely, measure it fairly, and separate genuine alpha from exposure to known risk factors.
What is the difference between a cross-sectional and a time-series signal?Junior
A cross-sectional signal ranks assets against each other at the same time: which stocks will outperform the others tomorrow. A time-series signal predicts one asset's own future return from its history: will this future go up or down.
They are built and judged differently. Cross-sectional signals are usually traded as long-short portfolios that are neutral to the market, so the market's direction drops out and what matters is the ranking; they are evaluated with rank information coefficients across many assets each day. Time-series signals, such as trend following, take directional exposure and are evaluated on the timing of each asset's own return.
A momentum effect can exist in both forms, and they behave differently: cross-sectional momentum is a bet on relative winners, while time-series momentum can be long everything in a rising market. Being clear which one you mean is the first thing an interviewer checks.
What is the information coefficient, and what is a good value?Junior
The information coefficient is the correlation between a signal and the subsequent returns it is trying to predict, usually measured cross-sectionally each period and then averaged. Most teams use the rank (Spearman) version so a few extreme names do not dominate.
Typical values for a daily equity signal are small, often a few hundredths. That sounds tiny, but across hundreds of stocks and many periods it can support a useful strategy. What matters as much as the mean is its consistency: I look at the mean IC divided by its standard deviation over time, the fraction of periods with positive IC, and how IC decays as the forecast horizon lengthens.
A high IC on a small or illiquid universe is less useful than a lower IC on names you can trade cheaply, so I would always quote it alongside the universe and the horizon.
Likely follow-up. How does IC relate to the expected information ratio of a strategy?
State the fundamental law of active management and explain its limits.Mid
It says the information ratio is roughly the information coefficient times the square root of breadth, where breadth is the number of independent bets per year. A small IC applied to many independent bets can give a good information ratio.
The limits are in the word independent. Five hundred stocks traded daily are not 126,000 independent bets a year, because stocks share sector and market exposures and signals persist from day to day. Breadth is usually much smaller than a naive count. The law also ignores transaction costs and constraints: a transfer coefficient term is often added to show how much of the ideal signal survives position limits and neutrality constraints.
I use it as a way of thinking rather than a calculation: it explains why a slightly better IC on a broad universe can beat a much higher IC on a narrow one, and why diversifying across uncorrelated signals helps.
How do you check whether a new signal is just a known factor in disguise?Mid
I regress the signal's portfolio returns on known factor returns (market, size, value, momentum, low volatility, industry) and see how much return is left in the intercept. I also neutralise the signal itself cross-sectionally against factor exposures and re-measure its IC.
If the signal's returns are mostly explained by momentum, say, it is not new alpha; it is an expensive way of getting a factor I can buy more cheaply. The residual IC after neutralisation and the intercept of the regression are the honest measures of novelty.
I would also check correlation with the team's existing signals, because the useful question in a real team is incremental value: does adding this improve the combined forecast after costs. A signal with modest standalone performance but low correlation to everything else can be worth more than a strong one that duplicates what you already have.
Likely follow-up. What does neutralising a signal against sector actually do to the numbers?
What does it mean to demean or z-score a signal cross-sectionally, and why do it?Junior
Each day I take the raw signal across the universe, subtract its cross-sectional mean (and sometimes divide by its cross-sectional standard deviation) so it has mean zero and unit spread on every date.
This makes the signal comparable through time, removes the market-wide component so a long-short portfolio built from it is roughly dollar neutral, and stops a volatile day from dominating the weights. Often I first clip extreme values, then rank or z-score, then optionally neutralise against sector and size.
The pitfall is computing the mean or standard deviation over the whole sample instead of per date, which leaks future information. Every normalisation must use only data available at the time of the decision.
How do you measure signal decay, and why does it matter?Mid
I compute the IC, or the return of a simple portfolio, at a range of forward horizons: next 1, 2, 5, 10, 20 days, or next few seconds to minutes for intraday signals. The decay curve shows how quickly the predictive power fades.
It matters for three decisions. Holding period: you should hold roughly as long as the signal still carries information net of costs. Turnover: a fast-decaying signal forces high turnover and therefore high costs, so it needs a large IC per trade. Execution delay: if half the edge is gone in the first minute, a signal computed at the close and traded the next morning is not viable, so the decay curve tells you how fast the trading system must be.
I would also look at IC lagged by one period to see how much is lost from a realistic delay between signal and trade.
How would you combine several alpha signals into one forecast?Senior
I start simple: z-score each signal, weight them equally or by their historical IC, and measure the combined IC out of sample. Equal weighting is a strong baseline because estimated weights are noisy.
If I have enough data I fit a regression of forward returns on the signals, ideally with shrinkage such as ridge, using a rolling or expanding window so weights only use past data. Correlated signals get split weight automatically, which a simple IC weighting does not do. Non-linear models can help when signals interact, but they overfit easily and need a strict validation set.
The trade-offs are stability against fit and interpretability against accuracy. I would always compare the combination against the equal-weight baseline after costs, and watch whether weights jump around between refits, which usually means the model is fitting noise.
Likely follow-up. When would you combine signals at the portfolio level rather than the forecast level?
Describe a clean research workflow in Python for a daily cross-sectional signal.Senior
Data is stored in long format keyed by date and asset, with a point-in-time universe so delisted stocks remain. I build the signal from data known at each date, lagging anything published with a delay, then clip, rank and neutralise it per date with a groupby on date.
Forward returns are computed separately and aligned explicitly, for example by shifting returns back by one period within each asset, so the join between signal and outcome is the only place where future data enters. I evaluate with per-date rank IC, quantile portfolio returns, turnover and decay, all split into in-sample and out-of-sample periods.
Engineering habits matter: vectorised pandas and numpy, no row loops over a large panel, functions that take a date range so nothing peeks forward, unit tests on small hand-made frames, and saved configuration for every run. See Python for Data Analysis Roadmap for the library skills behind this, and Python Roadmap for the language itself.
Backtesting, biases and overfitting
This is where most research candidates are filtered: interviewers hand you a beautiful backtest and wait to see whether you find what is wrong with it.
What is look-ahead bias? Give three concrete ways it creeps into a backtest.Junior
Look-ahead bias is using information in a simulated decision that would not have been available at that moment. It makes backtests look better than any real strategy can be.
Common routes: trading at the same close price that was used to compute the signal, which assumes you could act instantly at a price you only learn at the end; using fundamental data on its period-end date rather than its publication date, which can be weeks later; and normalising features with statistics computed over the full sample, such as a global mean or a scaler fitted on all data. Others include backward-filling missing values and using today's index membership to choose the historical universe.
My defence is structural: compute signals with an explicit as-of time, align them to returns that start strictly after that time, and write a test that shuffles future returns and checks performance drops to zero.
What is survivorship bias and how do you avoid it?Junior
Survivorship bias comes from testing only on assets that exist today. Companies that went bankrupt, were delisted or were acquired drop out, and they are disproportionately the poor performers, so the backtest universe is quietly tilted towards winners.
It inflates long-only and value-style results in particular, because a cheap stock that later went to zero is exactly the trade that is missing. The fix is a point-in-time security master: for every historical date, the universe is the set of instruments that were tradable on that date, with their returns through delisting, including the delisting return.
A related bias is using a current index constituent list for the past. Index changes are themselves predictable events, so membership must be historical too.
What is data snooping, and how is it different from overfitting a single model?Mid
Overfitting is a single model with too many parameters for the data, fitting noise in the training set. Data snooping is subtler: many researchers, or one researcher over months, try many ideas on the same history and keep what worked. Each individual model can be simple, but the selection process has fitted the noise.
The defences differ. For overfitting, regularisation, fewer parameters and out-of-sample validation work. For snooping, a hold-out set only helps if it has truly never been looked at, so I keep a final period locked, log the number of trials, apply multiple-testing corrections, and favour ideas with an economic reason decided before seeing the data.
A sign of snooping is a strategy with oddly specific parameters, such as a 17-day lookback with a 0.83 threshold, whose performance collapses when the parameters change slightly.
Likely follow-up. How would you check whether a parameter choice is fragile?
How should you split time-series data for validation?Mid
Never randomly. Random k-fold cross-validation puts future observations in the training set for past test points, and with autocorrelated features and overlapping labels it leaks information heavily.
I use walk-forward validation: train on data up to time t, test on the following block, roll forward and repeat, so every test point is strictly after its training data. Where labels span several days, I add a purge (drop training samples whose label window overlaps the test block) and an embargo (a gap after the test block) so the model cannot learn from neighbouring days.
I keep a final out-of-sample period untouched until the model is frozen. The number I report is the walk-forward result across many blocks, with its variation, not the best block.
How do you model transaction costs in a backtest?Mid
I include three parts: explicit fees and commissions, the half-spread you pay when crossing, and market impact, which grows with the size of the trade relative to volume.
A common impact model is that the cost per share scales with volatility times the square root of order size over daily volume. For a daily strategy I charge each trade a half-spread plus a square-root impact term, using the spread and volume known at the time. For intraday strategies I simulate fills against the book: a passive order only fills if the price trades through it, and with queue position modelled where possible.
I report results across a range of cost assumptions, including double the estimate, because the cost model is the least certain input. A strategy whose Sharpe goes from 2 to zero when costs double is a fragile strategy.
Likely follow-up. How do you know your cost model is right?
A backtest shows a Sharpe ratio of 4 for a daily equity strategy. What do you check first?Senior
I assume a bug until proven otherwise, because daily strategies with that Sharpe after costs are extremely rare.
First, timing: is the signal using the close it trades at, or data published after the decision time. I shift the signal by an extra day; if most of the performance disappears, I have look-ahead. Second, universe: survivorship, and whether tiny illiquid names drive the result, which I check by restricting to liquid names. Third, costs and capacity: is turnover huge, and what happens with realistic spreads and impact.
Then data errors: bad prices, splits not adjusted, stale prices that create fake mean reversion. Then the research process: how many variants were tried. Finally, concentration: does the return come from a handful of days or names. Only after all of that would I look at out-of-sample periods and believe any part of the number.
How would you design a backtester that makes look-ahead bias hard to introduce?Senior
I would make it event-driven at the core, with a clock that only releases data whose availability timestamp is at or before the current time, and strategies that can only read through that interface.
Every dataset carries two times: the event time and the time we knew it. Fundamentals, corrections and index changes are stored as point-in-time records, so a query for a past date returns what was known then, not the revised value. Orders are submitted at time t and filled by a separate fill simulator against later market data, with latency added, so a strategy cannot trade at the price that triggered it.
For fast research, vectorised backtests are still useful, so I would have both, and run the same strategy through each as a consistency check. I would add tests that inject a known future-dependent signal and assert the engine refuses or that results collapse when shifted.
What is turnover, and why do researchers care about it?Junior
Turnover is how much of the portfolio is traded per period, usually the sum of absolute weight changes, sometimes halved so that replacing the whole book once counts as 100%. A daily turnover of 20% means you trade a fifth of your gross book each day.
It matters because costs scale with turnover. The net return is roughly the gross return minus turnover times the cost per unit traded, so a strategy with a small edge per trade and high turnover can be unprofitable. Turnover also limits capacity: the more you trade, the more of daily volume you consume.
I reduce turnover by smoothing the signal, adding a no-trade band around current positions, or optimising with an explicit cost penalty. The goal is not minimal turnover but the best return after costs.
Portfolio construction, risk and costs
Turning a forecast into positions is where most of the money is made or lost, so interviewers probe sizing, neutrality, risk models and the cost of trading.
How do you calculate a Sharpe ratio, and what are its weaknesses?Junior
The Sharpe ratio is the mean excess return divided by the standard deviation of returns, over the same period, then annualised. For daily returns you multiply by the square root of 252, assuming independent returns.
Weaknesses: it treats upside and downside volatility the same; it ignores fat tails and skew, so a strategy that sells insurance can show a high Sharpe until the crash; it is biased upwards when returns are autocorrelated, as with smoothed or illiquid prices; and it is a noisy estimate with a wide confidence band over short periods.
So I report it together with maximum drawdown, skewness, the worst days and the Sortino ratio, and I check the autocorrelation of returns. For a market-neutral strategy the risk-free rate matters little, but for directional strategies it must be subtracted consistently.
Likely follow-up. Why might a Sharpe ratio computed on monthly returns differ from one computed on daily returns?
What is mean-variance optimisation, and why do practitioners rarely use the raw version?Mid
Mean-variance optimisation chooses weights to maximise expected return minus a risk-aversion penalty times portfolio variance. Unconstrained, the optimal weights are proportional to the inverse covariance matrix times the expected returns.
In practice the raw version is an error maximiser. Expected returns are estimated very noisily, and inverting a sample covariance matrix amplifies noise in its smallest eigenvalues, so the optimiser takes huge offsetting positions in assets that look slightly different by accident. Results are unstable from one rebalance to the next.
Practitioners use a factor or shrunk covariance matrix, shrink expected returns towards zero or towards a prior, add constraints (position limits, sector and factor neutrality, gross and net exposure), and include a transaction cost term so the optimiser trades only when the improvement pays for the cost.
Why use a factor risk model instead of a sample covariance matrix?Mid
A sample covariance matrix for N assets has about N squared over two parameters. With 2,000 stocks and a few years of daily data you have far fewer observations than parameters, so the matrix is noisy and often singular.
A factor model says each asset's return is its exposures to a small number of factors times the factor returns, plus a specific return that is independent across assets. Covariance becomes B F B-transpose plus a diagonal matrix D, where B holds exposures, F is the small factor covariance and D holds specific variances. That needs far fewer parameters, is always positive definite if D is positive, and gives interpretable risk: how much comes from market, sector, style or stock-specific bets.
The trade-off is model error: risk that does not load on any chosen factor is treated as diversifiable, so crowded trades with a hidden common driver can be underestimated.
What does it mean for a portfolio to be dollar neutral, beta neutral and sector neutral?Junior
Dollar neutral means the long market value equals the short market value, so net exposure is zero in money terms. Beta neutral means the beta-weighted exposures cancel, so the portfolio has no expected sensitivity to the market. Sector neutral means net exposure within each sector is zero.
They are different. A dollar-neutral book that is long high-beta tech and short low-beta utilities still has a large positive market beta. Which neutrality you want depends on the signal: if the alpha is about stock selection within industries, sector neutrality removes risk the signal says nothing about.
Each constraint costs a little expected return and reduces risk you are not paid for, which usually improves the risk-adjusted result. The test is whether the information ratio rises after neutralisation.
How do you size positions for a strategy with a time-varying volatility?Mid
I target a constant level of risk, scaling the position by the inverse of a forecast volatility. If volatility doubles, the position halves, so the expected contribution to portfolio risk stays roughly the same.
For the forecast I use an exponentially weighted estimate of recent returns, with a half-life chosen to balance responsiveness against noise, and a floor so a quiet period does not create a huge position. Across assets, inverse-volatility weighting is the simple version; using the full covariance accounts for correlations as well.
Volatility targeting usually improves the Sharpe ratio because volatility clusters and is more predictable than returns. The risks are that you cut positions after a shock and miss the rebound, and that you scale up just before volatility returns, which is why caps on leverage matter.
Explain market impact and the trade-off between trading quickly and slowly.Senior
Market impact is the price move your own trading causes. Part of it is temporary, the cost of demanding liquidity right now, and part is permanent, because the market reads your trading as information.
Trading quickly means a large temporary impact because you take liquidity aggressively. Trading slowly reduces impact but exposes you to price risk while the order is unfinished, and to information leakage as others detect you. The optimal schedule balances expected impact cost against the variance of execution cost, weighted by risk aversion. That is the structure of the Almgren and Chriss framework, where a more risk-averse trader front-loads the schedule.
Empirically, impact grows roughly with the square root of participation, so splitting an order into smaller pieces over time helps, but with diminishing returns. A strategy's capacity is the size at which impact eats its alpha.
Likely follow-up. How would you estimate the impact parameters from your own fills?
How do you decide the capacity of a strategy?Senior
Capacity is the capital at which net performance falls to an unacceptable level because costs grow with size while the gross alpha does not.
I simulate the strategy at increasing capital levels with a cost model in which impact depends on trade size relative to each asset's daily volume. At each level I also impose participation limits, such as never more than a few percent of daily volume in a name. Plotting net Sharpe and net profit against capital gives a curve: profit rises, peaks and falls. The capacity is somewhere before the peak, where the marginal dollar still earns enough.
Signals concentrated in small, illiquid names or with high turnover have low capacity. I would also check crowding: if similar strategies trade the same names, real impact will be higher than the model, especially when everyone exits at once.
Market microstructure and the order book
Both researchers and systems engineers must understand how orders interact in an exchange, because fills, costs and latency all come from these mechanics.
Explain the structure of a limit order book and the terms bid, ask, spread, depth and mid.Junior
A limit order book holds resting buy and sell limit orders at each price. The best bid is the highest price a buyer is willing to pay, the best ask the lowest price a seller will accept. The spread is the ask minus the bid, the mid is their average, and depth is the quantity resting at each price level.
An incoming order that crosses the spread, a buy at or above the best ask, trades against resting orders and removes liquidity. An order that does not cross rests in the book and adds liquidity. Most exchanges match by price-time priority: best price first, then earliest arrival at that price.
Depth and spread together describe liquidity. A tight spread with little depth is cheap for small orders and expensive for large ones, which is why researchers look at several levels, not only the top of the book.
Compare market, limit, IOC, FOK, post-only and stop orders.Junior
A market order trades immediately at the best available prices, with certainty of execution but not of price. A limit order sets a worst acceptable price and rests if it cannot trade. Immediate or cancel trades what it can at once and cancels the rest; fill or kill must fill completely at once or not at all.
Post-only is a limit order that is rejected or repriced if it would take liquidity, which market makers use to be sure they earn the maker rebate and never cross. A stop order becomes active once the market trades at a trigger price; it is used to cut losses but can execute far from the trigger in a fast market.
Exchanges also offer iceberg orders that show only part of their size, and auction-only orders for opening and closing auctions. The exact behaviour differs by venue, so a good engineer reads the venue's rules rather than assuming.
What is adverse selection for a market maker?Mid
Adverse selection is the cost of trading with someone who knows more than you. When a market maker's bid is hit, it is more likely than average that the price is about to fall, because informed sellers choose to sell before bad news. Fills arrive exactly when you would rather not have them.
This is why the spread exists even with no processing cost: the market maker earns the spread on uninformed flow and loses on informed flow, and the spread must cover the losses. Measuring it is straightforward: look at the mid price a short time after each fill. If the mid consistently moves against you after fills, your markouts are negative and you are being adversely selected.
Responses include widening quotes when signals suggest informed flow, skewing quotes based on order book imbalance, cancelling faster after market moves, and avoiding flow from counterparties whose fills have bad markouts.
Likely follow-up. How would you compute markouts from a fill log?
What is the microprice, and why might it be a better fair value than the mid?Mid
The microprice weights the bid and ask by the opposite side's size: it is the bid times the ask size plus the ask times the bid size, divided by the total size. When the bid has much more size than the ask, the microprice sits closer to the ask.
The intuition is that a thin ask is more likely to be consumed first, so the next mid is more likely to be higher. Order book imbalance is one of the most reliable very short-horizon predictors of the next price move, and the microprice folds it into a single fair value estimate.
The caveats: displayed size can be misleading because of hidden and iceberg orders and fleeting quotes, imbalance predicts only very short horizons, and its usefulness depends on the tick size. It works best in instruments where the spread is usually one tick and queues are long.
Why does queue position matter, and how does tick size affect it?Mid
Under price-time priority, a passive order at a given price fills only after all orders ahead of it at that price. Being at the front of the queue means you fill more often and your fills are less adversely selected, because you get filled by ordinary flow before the level is swept.
In large-tick instruments, where the spread is nearly always one tick and the price levels hold a lot of size, queue position is valuable and a large part of the market-making game is getting to the front quickly when a new level forms. In small-tick instruments the spread is several ticks wide, queues are short, and competition shifts to price improvement instead.
For backtesting, ignoring queue position overstates fill rates for passive orders. A reasonable simulator estimates your place in the queue from the size ahead when you joined and the trades and cancels since.
What is the difference between price-time and pro-rata matching?Junior
Price-time priority fills orders at the best price in arrival order: the first order at a price fills first. Pro-rata allocates an incoming order across all resting orders at the best price in proportion to their size.
The incentives differ. Price-time rewards speed and being early in the queue. Pro-rata rewards size, so participants tend to post larger orders than they really want, expecting to receive only a fraction, which creates its own risk if a large order hits them. Some markets use a mix, for example a priority allocation to the order that improved the price, then pro-rata for the remainder.
For a trading system the matching rule changes the strategy and the simulator, so it must be modelled for each venue.
How would you build an order book from a market data feed, and what can go wrong?Senior
For an order-by-order feed, I keep a map from order identifier to order (side, price, remaining size), and per side a price-indexed structure of levels with aggregate size and, if needed, a queue of orders. Each message (add, modify, cancel, execute, delete) updates both the order map and the level.
Feeds are usually UDP multicast with sequence numbers, so the main risk is a gap. On a gap, the book is no longer trustworthy: I mark the instrument stale, stop trading it, and recover from a snapshot channel or a retransmission service, then replay buffered messages after the snapshot's sequence number. Many venues send the same feed on two lines, A and B, and the handler arbitrates, taking whichever copy of each sequence number arrives first.
Other failure modes: a crossed book after a missed message, integer overflow in price fields, assuming one instrument per channel, and doing allocation or logging on the hot path. I test with recorded captures, including deliberately dropped packets.
Likely follow-up. How do you know your book is correct in production?
Execution algorithms and FIX
Execution questions test whether you understand how a parent order becomes child orders, how cost is measured, and how orders actually flow over the wire.
What are TWAP and VWAP algorithms, and when would you use each?Junior
TWAP slices an order evenly over a time window, for example one child order every minute of the same size. VWAP slices in proportion to the expected volume profile over the day, trading more when the market usually trades more, such as near the open and close.
VWAP is used when the client is measured against the day's volume-weighted average price, which is common for large institutional orders, because matching the volume profile tends to keep the execution close to that benchmark. TWAP is simpler and more predictable, useful when volume forecasts are poor or in instruments without a reliable profile.
Both are schedules, not strategies: a real implementation adds randomisation so the pattern is less detectable, limit prices, a participation cap, and logic to choose between passive and aggressive child orders.
What is implementation shortfall, and how do you measure execution quality?Mid
Implementation shortfall is the difference between the price when the decision was made, usually the arrival price, and the average price actually achieved, plus the opportunity cost of any part not filled. It is the true cost of turning a decision into a position.
Measurement is transaction cost analysis. For each parent order I compute slippage against arrival price, against interval VWAP and against the close, in basis points, signed so that positive is a cost. I break it into spread paid, impact and timing. Averaged over many orders and normalised by volatility and size, it shows whether an algorithm or broker is better.
Benchmarks can be gamed: VWAP slippage looks good if you trade slowly in a trending market, while shortfall shows the real cost. That is why I prefer arrival price as the main benchmark for alpha-driven trading.
Likely follow-up. Why can VWAP slippage look good while the trade was actually expensive?
Describe the FIX messages involved in sending, amending and cancelling an order.Junior
A new order is a NewOrderSingle, message type D, carrying a client order id in tag 11, the symbol, side in tag 54, quantity in tag 38, order type and price in tag 44. The venue replies with ExecutionReports, message type 8, for acknowledgement, partial fills, fills and rejections, with OrdStatus in tag 39 and ExecType in tag 150.
A cancel request is message type F and an amend is an OrderCancelReplaceRequest, message type G. Both carry a new client order id in tag 11 and the id of the order being changed in tag 41, OrigClOrdID. If a cancel cannot be done, for example because the order already filled, the venue sends an OrderCancelReject, message type 9.
The detail interviewers like is that cancel and amend races are normal: a fill can arrive after you sent a cancel, so the order state machine must handle fills on orders that are pending cancel.
Likely follow-up. What happens if a cancel and a fill cross on the wire?
How does a FIX session handle sequence numbers and recovery?Mid
Each side numbers its outgoing messages in tag 34, MsgSeqNum, starting from 1 at the beginning of a session. The receiver checks every number. If one is higher than expected, messages were missed and it sends a ResendRequest, message type 2, for the range. If it is lower than expected without the possible duplicate flag, that is a serious error and the session is usually terminated.
The sender replays the requested messages with PossDupFlag set, and may use a SequenceReset in gap-fill mode, message type 4, to skip administrative messages that should not be resent, such as old heartbeats. Logon is message type A, heartbeat 0 and test request 1; heartbeats at the agreed interval detect a silent connection.
For an order gateway, the important rule is that order messages are persisted with their sequence numbers before or as they are sent, so a restart can resume the session correctly instead of resetting and losing the state of live orders.
What does an order state machine need to handle in a trading gateway?Mid
It tracks each order from creation to a terminal state: pending new, new or acknowledged, partially filled, filled, pending cancel, cancelled, pending replace, replaced, rejected and expired. Every transition comes from either our request or the venue's report.
The tricky cases are races. A fill can arrive while a cancel is pending, so filled quantity must be accepted in any non-terminal state. A replace can be rejected, so the old order remains live at its old price. Reports can arrive out of order across channels, so I key on cumulative quantity rather than incrementing on each fill. An unacknowledged order after a timeout must be treated as possibly live, never assumed dead.
I would make the state machine a pure function of events so it can be unit-tested with recorded sequences, and reconcile positions against the venue's drop copy at regular intervals.
How would you choose between passive and aggressive child orders in an execution algorithm?Senior
It is a trade-off between saving the spread and taking timing risk. A passive order earns the spread if it fills, but may not fill, and fills tend to come when the price is moving against you. An aggressive order pays the spread but executes with certainty.
I decide using how far behind schedule the algorithm is, a short-term price forecast such as order book imbalance or microprice, the spread in ticks, and the queue length. If the forecast says the price is about to move away, crossing now is cheaper than chasing later. If the queue is short and the forecast neutral, joining the bid is attractive. Near the end of the window, urgency grows and the algorithm crosses more.
The decision should be evaluated with markouts and fill-rate statistics, comparing realised cost against what the alternative choice would have cost.
What pre-trade risk checks must sit in front of an automated strategy?Senior
Every order must pass checks that are fast, deterministic and independent of the strategy's own logic. Typical checks: maximum order size and value; price collars that reject orders too far from a reference price, catching fat-finger and stale-price errors; position and exposure limits per instrument and in total; maximum open orders; message rate throttles to stay within venue limits; and checks on duplicate or runaway order sequences.
A kill switch must be able to cancel all orders and block new ones in one action, both manually and automatically on triggers such as a loss limit or a disconnection from the risk system. Checks must be evaluated on the current state including orders in flight, not just confirmed positions, otherwise a burst of orders can breach limits before fills arrive.
For low latency the checks are written as simple comparisons on precomputed limits, with no allocation, and their latency is measured like any other hot-path code.
Low-latency C++ and trading systems
Systems interviews check that you know where time goes in a modern machine and can write C++ that is fast because it is predictable, not because it is clever.
What is a cache line, and why does data layout matter for a trading system?Junior
A cache line is the unit the CPU moves between memory and cache, 64 bytes on most current x86 and many Arm processors. Reading one byte brings in the whole line, so data used together should live together.
A miss to main memory costs on the order of a hundred nanoseconds, far more than the arithmetic in a typical handler. So hot-path data structures favour contiguous arrays over linked nodes, pack the fields read on every message into one or two lines, and keep rarely used fields elsewhere. An order book stored as an array of levels indexed by price offset is usually faster than a tree because walking it is predictable.
The other side is false sharing: two threads writing different variables on the same line force the line to bounce between cores. Padding or aligning such variables to separate lines fixes it.
Why do low-latency systems avoid dynamic memory allocation on the hot path?Mid
A general-purpose allocator call has unpredictable latency: it may take a lock, search free lists, or ask the operating system for memory, which can involve a page fault. The average is small, but the tail is what loses trades.
So hot-path code preallocates everything at start-up: object pools for orders, fixed-capacity ring buffers for messages, and containers with reserved capacity. Memory is touched once at start-up so pages are already mapped, often with huge pages to reduce translation lookaside buffer misses. Standard library types that allocate implicitly, such as std::string concatenation, std::function with large captures and growing vectors, are kept off the critical path.
The same reasoning applies to anything with variable cost: exceptions, logging that formats strings, and system calls are moved to another thread or done in a non-critical phase.
Likely follow-up. How would you log from a hot thread without slowing it down?
Explain memory ordering for a single-producer single-consumer queue.Mid
The producer writes the element into the slot and then publishes the new tail index with a release store. The consumer reads the tail with an acquire load; if it sees the new index, the release-acquire pair guarantees it also sees the element written before it. The consumer publishes its head index the same way so the producer knows when a slot is free.
No locks or compare-and-swap are needed because each index has a single writer. Relaxed ordering is enough for a thread reading its own index. Using sequentially consistent operations everywhere also works but adds fences that are unnecessary on some architectures.
For speed, the head and tail live on separate cache lines to avoid false sharing, and each side keeps a cached copy of the other side's index, refreshing it only when the queue looks full or empty. That cuts cross-core traffic sharply. See the SPSC exercise below for a tested version.
What is kernel bypass networking, and what does it buy you?Mid
Kernel bypass lets an application read and write network packets directly from the network card's queues in user space, instead of going through the operating system's network stack and socket calls.
It removes system call overhead, interrupt handling, copies between kernel and user buffers and contention with other kernel work. Combined with a dedicated core that busy-polls the receive queue, it reduces both the median and, more importantly, the variance of packet latency. Some cards also timestamp packets in hardware, which gives accurate latency measurement.
The costs are a core spinning at full use, a more complex software stack, and often vendor-specific libraries. You also lose the kernel's protections and tools, so packet capture and monitoring must be built separately. It only helps if the rest of the path is also fast: a slow strategy loop behind a bypassed network card gains little.
How do you measure latency inside a trading system accurately?Mid
I timestamp at well-defined points: packet arrival (ideally a hardware timestamp from the network card), after decoding, after the strategy decision, and when the order is handed to the network card. The difference between the first and last is tick-to-trade.
Within one machine I use a cheap, monotonic counter such as the CPU's time stamp counter with a known frequency, read without serialising more than needed. I record values into a preallocated buffer and aggregate on another thread, reporting percentiles rather than means. Wire-to-wire measurement uses a capture device that timestamps packets on both sides, which also includes the time spent in the network card and the stack.
Pitfalls: measurement code that itself allocates or takes a lock, clock differences between cores or machines, and averaging away the tail.
How would you set up a Linux machine to run a latency-critical trading process?Senior
I isolate cores for the critical threads so the scheduler and most interrupts never run there, pin each critical thread to its own isolated core, and route the network card's interrupts away from them. Power-saving states and frequency scaling are disabled or fixed so the core does not slow down while idle.
Memory is allocated on the NUMA node local to the network card and the critical cores, using huge pages, and locked so it cannot be swapped. Hyper-threading siblings of critical cores are left idle so they do not share execution resources. Unneeded services and timers are reduced.
I would verify each change by measuring tail latency before and after, because tuning by habit can hurt. Interviewers also like hearing that the configuration is automated and checked at start-up, so a machine that came back from maintenance with the wrong settings refuses to trade. See Linux Roadmap for the operating system background.
Why might you avoid virtual functions on a hot path, and what would you use instead?Mid
A virtual call is an indirect branch through a table. On its own it costs little if the branch predictor guesses the target, but it blocks inlining, which stops the compiler from optimising across the call, and a mispredicted target costs a pipeline flush.
In a hot path where the concrete type is known at build or start-up time, I use templates: the curiously recurring template pattern for static polymorphism, or a strategy type passed as a template parameter so the whole handler is compiled as one unit. Where a choice is made at run time among a few types, std::variant with a visit, or a switch, keeps calls direct and inlinable.
I would not remove virtual functions everywhere. Outside the critical path they make code easier to extend. Measure first, and change what the profile shows.
What is kdb+/q used for in trading, and what does a typical setup look like?Senior
kdb+ is a column-oriented, in-memory time-series database, and q is its vector query language. Trading firms use it to store and query tick data, both live and historical, because queries over long time-ordered columns are very fast.
A common architecture has a tickerplant that receives the feed and logs it, a real-time database holding today's data in memory, and a historical database on disk partitioned by date. Subscribers such as analytics or monitoring processes receive updates from the tickerplant. At end of day, the real-time data is written to the historical database.
Useful features to mention are as-of joins, which attach the prevailing quote to each trade, and concise aggregation. For example:
select vwap:size wavg price by sym from trade where date=2024.01.02q evaluates right to left with no operator precedence, which catches newcomers out. For SQL foundations see SQL Roadmap.
Likely follow-up. Why are as-of joins so useful for trade and quote analysis?
How would you make a trading system deterministic and replayable?Senior
I make the core logic a function of its inputs: every market data message, order response and timer event is an input event with a sequence number and timestamp, and the strategy reads time only from the event, never from the system clock. Inputs are recorded in order at the boundary.
With that, any production session can be replayed through the same binary and must produce the same outputs, which gives exact reproduction of bugs, regression testing against recorded days, and a check that a new version only changes what was intended. Single-threaded core logic, fed by queues from I/O threads, makes this much easier than shared-state concurrency.
Sources of hidden non-determinism to remove: iteration over unordered containers whose order depends on addresses, uninitialised memory, floating-point differences across builds, and timers driven by the wall clock. The same design also makes the backtester and the live system share one code path.
Coding exercises
Set a timer for 30 minutes per exercise. Write the code in an editor, compile it, and test the edge cases before you open the solution.
- Maximum drawdown of an equity curveEasy · Python
Given a list of portfolio values over time, return the maximum drawdown as a fraction of the running peak, together with the indices of the peak and the trough. For
[100, 120, 90, 130, 104, 140]the answer is 0.25, from index 1 to index 2. Do it in one pass.Hints
- Track the highest value seen so far and its index.
- At each point the drawdown is the fall from that running peak.
Solution and explanation
def max_drawdown(equity): """Return (drawdown_fraction, peak_index, trough_index) for an equity curve.""" if not equity: raise ValueError("empty equity curve") peak_val, peak_i = equity[0], 0 best = (0.0, 0, 0) for i, v in enumerate(equity): if v > peak_val: peak_val, peak_i = v, i if peak_val > 0: dd = (peak_val - v) / peak_val if dd > best[0]: best = (dd, peak_i, i) return best if __name__ == "__main__": curve = [100, 120, 90, 130, 104, 140] dd, p, t = max_drawdown(curve) print(round(dd, 4), p, t) # 0.25 1 2The loop keeps the running peak and, at every point, measures how far the current value is below it. The largest such fall is the maximum drawdown. It is one pass, O(n) time and O(1) space.
The detail interviewers check is that the peak used for a drawdown must come before the trough. Taking the global maximum and global minimum is wrong: in the example, the global minimum 90 is after the peak 120, which happens to work, but in a curve that rises steadily to its highest point the global minimum is at the start and there is no drawdown at all. Updating the peak first, then measuring, handles this. Note also that the drawdown from 130 to 104 is 0.2, smaller than 0.25, so the first fall wins even though it is not the largest in absolute terms.
Edge cases worth mentioning: an empty list (raise), a non-positive peak (skip, because a fraction is meaningless), and a curve that only rises (drawdown zero). Mention that drawdown is usually reported alongside its duration and the time to recover, which you can track with the same loop.
- Rolling VWAP over the last N tradesEasy · Python
Trades arrive one at a time as price and quantity. After each trade, return the volume-weighted average price of the most recent N trades. Each update must be O(1). With N = 3 and trades (10.0, 100), (10.5, 200), (10.2, 100), (11.0, 400), the outputs are 10.0, 10.3333, 10.3 and 10.7429.
Hints
- Keep running sums of price times quantity and of quantity.
- A deque lets you drop the oldest trade in O(1).
Solution and explanation
from collections import deque class RollingVWAP: """VWAP over the last `window` trades, O(1) per update.""" def __init__(self, window): if window <= 0: raise ValueError("window must be positive") self.window = window self.trades = deque() self.pv = 0.0 # sum of price * volume self.vol = 0 # sum of volume def update(self, price, qty): self.trades.append((price, qty)) self.pv += price * qty self.vol += qty if len(self.trades) > self.window: old_p, old_q = self.trades.popleft() self.pv -= old_p * old_q self.vol -= old_q return self.pv / self.vol if self.vol else None if __name__ == "__main__": v = RollingVWAP(3) for p, q in [(10.0, 100), (10.5, 200), (10.2, 100), (11.0, 400)]: print(round(v.update(p, q), 4)) # 10.0, 10.3333, 10.3, 10.7429VWAP is the sum of price times quantity divided by the sum of quantity. Keeping both sums and a deque of the trades in the window lets each update add the new trade and subtract the one that falls out, so the cost per trade is constant regardless of N.
Things the interviewer listens for: the zero-volume case (return None rather than divide by zero); the choice between a window of N trades and a window of time, which is more common in practice and needs the deque to hold timestamps with a while loop for eviction; and floating-point drift. Repeatedly adding and subtracting floats accumulates rounding error over millions of updates. In production you would either keep prices as integer ticks and quantities as integers so the sums are exact, or recompute the sums from the window periodically. In C++ the same design uses a fixed-size ring buffer instead of a deque so there is no allocation on the hot path.
- Rank information coefficient and annualised Sharpe from scratchMedium · Python
Without numpy or pandas, write
rank_ic(signal, fwd_returns)that returns the Spearman correlation between a signal and next-period returns for one date, skipping pairs where either value is missing (None) and giving tied values their average rank. Also writeannualised_sharpe(returns, periods_per_year). For the signal [1, 2, 3, 4, None] and returns [0.1, 0.3, 0.2, 0.4, 0.5], the rank IC is 0.8.Hints
- Spearman correlation is the Pearson correlation of the ranks.
- Sort indices by value, then walk runs of equal values to assign the mean rank.
Solution and explanation
import math def ranks(xs): """Average ranks (1-based), ties share the mean rank.""" order = sorted(range(len(xs)), key=lambda i: xs[i]) r = [0.0] * len(xs) i = 0 while i < len(order): j = i while j + 1 < len(order) and xs[order[j + 1]] == xs[order[i]]: j += 1 avg = (i + j) / 2 + 1 for k in range(i, j + 1): r[order[k]] = avg i = j + 1 return r def pearson(a, b): n = len(a) ma, mb = sum(a) / n, sum(b) / n cov = sum((x - ma) * (y - mb) for x, y in zip(a, b)) va = sum((x - ma) ** 2 for x in a) vb = sum((y - mb) ** 2 for y in b) if va == 0 or vb == 0: return float("nan") return cov / math.sqrt(va * vb) def rank_ic(signal, fwd_returns): """Spearman correlation between today's signal and next-period returns.""" pairs = [(s, r) for s, r in zip(signal, fwd_returns) if s is not None and r is not None] if len(pairs) < 3: return float("nan") s, r = zip(*pairs) return pearson(ranks(s), ranks(r)) def annualised_sharpe(period_returns, periods_per_year=252): n = len(period_returns) mu = sum(period_returns) / n sd = math.sqrt(sum((x - mu) ** 2 for x in period_returns) / (n - 1)) return mu / sd * math.sqrt(periods_per_year) if __name__ == "__main__": print(ranks([3, 1, 3, 2])) # [3.5, 1.0, 3.5, 2.0] print(round(rank_ic([1, 2, 3, 4, None], [0.1, 0.3, 0.2, 0.4, 0.5]), 4)) # 0.8 print(round(annualised_sharpe([0.01, -0.005, 0.002, 0.004]), 3))The rank function sorts the indices by value and walks through runs of equal values, assigning each run the mean of the ranks it spans, which is the standard tie treatment. Spearman correlation is then the Pearson correlation of the two rank vectors. Overall cost is O(n log n) for the sort.
Missing data is dropped pairwise before ranking, which matters: ranking first and then dropping would leave gaps in the ranks. If fewer than three valid pairs remain, or one side is constant, the correlation is undefined and the function returns NaN rather than a misleading zero.
For the Sharpe ratio the sample standard deviation uses n minus 1, and annualisation multiplies by the square root of periods per year, which assumes independent returns. A strong candidate mentions that in a real study you compute the IC per date and then look at its mean, its standard deviation over time and the share of positive dates, and that rank IC is preferred because a few extreme returns do not dominate it.
- Moving-average crossover backtest without look-aheadMedium · Python
Write a backtest for a long-or-flat moving-average crossover on daily closes. Hold one unit when the fast simple moving average is above the slow one, otherwise hold nothing. The decision at the close of day i may only use closes up to day i and earns the return from day i to day i+1. Charge a cost in basis points of notional every time the position changes. Return total return and the number of trades.
Hints
- Be explicit about which close the signal uses and which return the position earns.
- Apply the cost before applying the next return.
Solution and explanation
def sma(xs, n, i): """Mean of xs[i-n+1 .. i]; uses only data up to and including i.""" return sum(xs[i - n + 1:i + 1]) / n def backtest_crossover(closes, fast, slow, cost_bps): """Long 1 unit when fast SMA > slow SMA, else flat. The signal computed at the close of day i is traded at the close of day i and earns the return from day i to day i+1. Costs are charged on every change in position, in basis points of traded notional. Returns (total_return, number_of_trades). """ if fast >= slow: raise ValueError("fast window must be shorter than slow window") pos, equity, trades = 0, 1.0, 0 for i in range(slow - 1, len(closes) - 1): target = 1 if sma(closes, fast, i) > sma(closes, slow, i) else 0 if target != pos: equity *= 1 - cost_bps / 1e4 * abs(target - pos) trades += 1 pos = target equity *= 1 + pos * (closes[i + 1] / closes[i] - 1) return equity - 1, trades if __name__ == "__main__": px = [10, 10, 10, 11, 12, 13, 12, 11, 10, 10, 11] r, n = backtest_crossover(px, 2, 3, cost_bps=10) print(round(r, 6), n)The loop starts once the slow average has enough history, computes the target position from data up to and including day i, pays the cost if the position changes, then applies the return from day i to day i+1. The last day is excluded because it has no next-day return. Each step recomputes the averages, so this simple version is O(n times slow); running sums make it O(n).
This exercise exists to test timing discipline. The classic bug is to apply day i's return to a signal computed from day i's close, which is look-ahead and makes almost any trend rule look profitable. Another is to forget that trading at the close you used for the signal assumes you can act at that exact price; a stricter version trades at the next open.
In the example the strategy makes zero gross return across the long period and loses exactly the two costs, giving minus 0.1999%, which is a useful check that costs are applied multiplicatively. A strong candidate also asks how splits and dividends are handled in the price series.
- Optimal stopping in a dice gameMedium · Python
You may roll a fair six-sided die up to n times. After each roll you either stop and receive the face value in pounds, or roll again; if you use all n rolls you receive the last one. Compute the expected payoff under optimal play as an exact fraction and the threshold rule at each stage. For n = 2 the value is 4.25; for n = 3 it is 14/3.
Hints
- Work backwards from the last roll.
- With k rolls left, keep a face if it is at least the value of continuing with k minus 1 rolls.
Solution and explanation
from fractions import Fraction def dice_game_value(rolls, faces=6): """You may roll a fair die up to `rolls` times and keep the last roll. Return the expected payoff under optimal stopping, as an exact fraction, plus the threshold rule: with k rolls left, keep a value >= threshold[k]. """ value = Fraction(0) # value with zero rolls left (not used) thresholds = {} for k in range(1, rolls + 1): if k == 1: new = Fraction(sum(range(1, faces + 1)), faces) thresholds[1] = 1 else: cont = value # value of continuing with k-1 rolls new = sum(max(Fraction(f), cont) for f in range(1, faces + 1)) / faces thresholds[k] = next(f for f in range(1, faces + 1) if f >= cont) value = new return value, thresholds if __name__ == "__main__": for n in (1, 2, 3): v, t = dice_game_value(n) print(n, v, float(v), t) # 1 7/2, 2 17/4, 3 14/3This is backward induction. With one roll left you must take what you get, worth 3.5. With two rolls left, you keep the first roll if it beats 3.5, so you keep 4, 5 or 6 and otherwise re-roll; the value is (4 + 5 + 6 + 3 times 3.5) over 6, which is 4.25. With three rolls you keep 5 or 6 because they beat 4.25, giving 14/3. Each stage is O(faces), so the total is O(n times faces).
Using Fraction keeps the answer exact, which avoids debates about rounding and makes it easy to verify by hand. Interviewers usually ask the n = 2 case verbally first, then ask for code to generalise it, then ask what changes if re-rolling costs money (subtract the cost from the continuation value) or if you can see the next roll before deciding (the value becomes an expected maximum).
The same structure, compare stopping now with the expected value of continuing, is the basis of American option pricing on a tree, which is a natural follow-up in quant interviews.
- VWAP schedule with whole lotsMedium · Python
Split a parent order of Q shares across time buckets in proportion to an expected volume profile. Every child order must be a whole number of lots and the children must add up to exactly Q. For Q = 10,000, lot size 100 and profile [0.2, 0.1, 0.1, 0.15, 0.45] the answer is [2000, 1000, 1000, 1500, 4500]. Handle profiles that do not sum to one.
Hints
- Work in lots, not shares.
- Floor every bucket, then hand the leftover lots to the buckets with the largest fractional remainders.
Solution and explanation
def vwap_schedule(total_qty, profile, lot=1): """Split a parent order across time buckets in proportion to an expected volume profile. Child sizes are whole lots and sum exactly to total_qty. Uses the largest-remainder method so rounding error never accumulates. """ if total_qty % lot: raise ValueError("total quantity must be a whole number of lots") if not profile or any(w < 0 for w in profile) or sum(profile) == 0: raise ValueError("profile needs non-negative weights with a positive sum") lots = total_qty // lot s = sum(profile) exact = [lots * w / s for w in profile] base = [int(x) for x in exact] left = lots - sum(base) # hand the leftover lots to the buckets with the largest fractional part, # ties broken by earlier bucket so the result is deterministic order = sorted(range(len(profile)), key=lambda i: (-(exact[i] - base[i]), i)) for i in order[:left]: base[i] += 1 return [b * lot for b in base] if __name__ == "__main__": print(vwap_schedule(10000, [0.2, 0.1, 0.1, 0.15, 0.45], lot=100)) print(vwap_schedule(7, [1, 1, 1])) print(sum(vwap_schedule(123400, [3, 7, 11, 2, 9, 5], lot=100)))Rounding each bucket independently can produce children that sum to more or less than the parent, which is a real production bug: the algorithm either over-executes or leaves shares unfilled. The largest-remainder method floors every ideal allocation, counts the lots left over, and gives one extra lot to each bucket with the largest fractional part. The total is exact and no bucket is more than one lot from its ideal. Sorting makes it O(k log k) for k buckets.
Ties are broken by bucket order so the output is deterministic, which matters for replay and testing. Inputs are validated: the total must be whole lots and the profile must have a positive sum with no negative weights.
Follow-ups: how to re-plan mid-day when the order is behind schedule (re-run on the remaining quantity and remaining buckets), how to cap any bucket at a maximum participation of forecast volume, and how to add randomisation so the schedule is less predictable to other participants.
- Lock-free single-producer single-consumer queueMedium · C++
Implement a bounded queue for passing messages from a market data thread to a strategy thread. Exactly one thread pushes and exactly one pops. No locks, no allocation after construction,
try_pushreturns false when full andtry_popreturns false when empty. Explain the memory ordering you use.Hints
- Monotonically increasing head and tail indices, masked by a power-of-two capacity, avoid the full-versus-empty ambiguity.
- Release on publish, acquire on observe.
- Cache the other side's index to cut cross-core traffic.
Solution and explanation
#include <atomic> #include <cstddef> #include <new> #include <type_traits> // Single-producer single-consumer bounded queue. N must be a power of two. template <typename T, std::size_t N> class SpscQueue { static_assert(N >= 2 && (N & (N - 1)) == 0, "N must be a power of two"); static_assert(std::is_trivially_copyable<T>::value, "keep T trivially copyable"); alignas(64) std::atomic<std::size_t> head_{0}; // next slot to read (consumer) alignas(64) std::size_t cached_tail_ = 0; // consumer's copy of tail alignas(64) std::atomic<std::size_t> tail_{0}; // next slot to write (producer) alignas(64) std::size_t cached_head_ = 0; // producer's copy of head alignas(64) T buf_[N]; public: bool try_push(const T& v) { // producer thread only const std::size_t t = tail_.load(std::memory_order_relaxed); if (t - cached_head_ == N) { cached_head_ = head_.load(std::memory_order_acquire); if (t - cached_head_ == N) return false; // full } buf_[t & (N - 1)] = v; tail_.store(t + 1, std::memory_order_release); // publish the slot return true; } bool try_pop(T& out) { // consumer thread only const std::size_t h = head_.load(std::memory_order_relaxed); if (h == cached_tail_) { cached_tail_ = tail_.load(std::memory_order_acquire); if (h == cached_tail_) return false; // empty } out = buf_[h & (N - 1)]; head_.store(h + 1, std::memory_order_release); // free the slot return true; } };Head and tail are free-running counters; the slot is the counter masked by N minus 1, and the queue is full when tail minus head equals N. Because unsigned arithmetic wraps, this stays correct even when the counters overflow, and no slot is wasted.
The producer writes the element, then stores the new tail with release ordering. The consumer loads tail with acquire ordering, so when it sees the new value it is guaranteed to see the element. The same pattern in reverse tells the producer a slot is free. Each index has one writer, so no compare-and-swap is needed and both operations are wait-free.
Performance details interviewers look for: head, tail and the cached copies are each on their own 64-byte line to avoid false sharing, and each side reads the other side's atomic only when its cached copy says the queue is full or empty. The test harness pushed five million sequence numbers across two threads, checked order and sum, and ran clean under ThreadSanitizer. Restricting T to trivially copyable types keeps the copy cheap and predictable.
- Validate and parse a FIX messageMedium · C++
Parse a FIX tag=value message delimited by the SOH character (0x01) into a list of tag and value pairs without copying the values. Reject the message unless it starts with tag 8 then tag 9, BodyLength (tag 9) equals the number of bytes from after the tag 9 field up to and including the SOH before tag 10, and CheckSum (tag 10) is the three-digit sum of all preceding bytes modulo 256 and is the last field.
Hints
- std::string_view lets values point into the original buffer.
- Remember where the body starts (just after the tag 9 field) and where tag 10 begins.
Solution and explanation
#include <cstdio> #include <string_view> #include <vector> #include <utility> struct Field { int tag; std::string_view value; }; // Parse one FIX tag=value message delimited by SOH (0x01). Checks that the // message starts 8=,9=, that BodyLength (9) matches and that CheckSum (10) // is the byte sum mod 256. Returns false on any malformed input. bool parse_fix(std::string_view msg, std::vector<Field>& out) { out.clear(); const char SOH = '\x01'; std::size_t pos = 0, body_start = 0, sum_end = 0; long body_len = -1; while (pos < msg.size()) { std::size_t field_start = pos, eq = msg.find('=', pos); if (eq == std::string_view::npos || eq == pos) return false; int tag = 0; for (std::size_t i = pos; i < eq; ++i) { if (msg[i] < '0' || msg[i] > '9' || tag > 100000) return false; tag = tag * 10 + (msg[i] - '0'); } std::size_t end = msg.find(SOH, eq + 1); if (end == std::string_view::npos) return false; std::string_view val = msg.substr(eq + 1, end - eq - 1); std::size_t idx = out.size(); if ((idx == 0 && tag != 8) || (idx == 1 && tag != 9)) return false; if (tag == 9) { body_len = 0; for (char c : val) { if (c < '0' || c > '9' || body_len > 1000000) return false; body_len = body_len * 10 + (c - '0'); } body_start = end + 1; } if (tag == 10) { if (end + 1 != msg.size() || val.size() != 3) return false; sum_end = field_start; if (static_cast<long>(sum_end - body_start) != body_len) return false; unsigned sum = 0; for (std::size_t i = 0; i < sum_end; ++i) sum += static_cast<unsigned char>(msg[i]); unsigned want = 0; for (char c : val) { if (c < '0' || c > '9') return false; want = want * 10 + (c - '0'); } if (sum % 256 != want) return false; } out.push_back({tag, val}); pos = end + 1; } return sum_end != 0; // must have ended with a valid tag 10 }The parser walks the buffer field by field: find the equals sign, convert the tag digits, find the next SOH, and take a string_view of the value so nothing is copied or allocated. It records where the body starts, right after the BodyLength field, and when it reaches tag 10 checks both the length and the checksum, and that tag 10 is the final field.
Every input is treated as hostile: empty tags, non-digit characters, missing SOH and absurdly large numbers all return false rather than crashing or overflowing. The test harness built a NewOrderSingle with a correct header and trailer, confirmed it parsed into eleven fields, and confirmed rejection when the checksum digit, a body byte or the length was altered, or when the final SOH was missing. It ran under AddressSanitizer and UBSan.
Discussion points: the checksum is a weak integrity check and exists mainly to catch framing errors; real engines parse incrementally from a TCP stream, so a message can be split across reads; and repeating groups make a flat list of fields insufficient for some message types.
- Sliding-window median of pricesHard · Python
Given a list of prices and a window size k, return the median of every window of k consecutive prices, much faster than sorting every window. Medians of an even window are the mean of the two middle values. Example: [1, 3, -1, -3, 5, 3, 6, 7] with k = 3 gives [1, -1, -1, 3, 5, 6]. A rolling median is used as an outlier-resistant filter for bad ticks.
Hints
- Keep the lower half in a max-heap and the upper half in a min-heap.
- Python's heapq cannot delete arbitrary items, so mark them as dead and remove them when they reach the top.
Solution and explanation
import heapq from collections import defaultdict def sliding_median(xs, k): """Median of every window of size k in O(n log k), two heaps + lazy delete.""" lo, hi = [], [] # lo: max-heap (negated), hi: min-heap dead = defaultdict(int) # values waiting to be removed lazily lo_n = hi_n = 0 # live element counts in each heap def prune(h, neg): while h and dead[-h[0] if neg else h[0]]: v = -heapq.heappop(h) if neg else heapq.heappop(h) dead[v] -= 1 def rebalance(): nonlocal lo_n, hi_n if lo_n > hi_n + 1: heapq.heappush(hi, -heapq.heappop(lo)); lo_n -= 1; hi_n += 1 prune(lo, True) elif hi_n > lo_n: heapq.heappush(lo, -heapq.heappop(hi)); hi_n -= 1; lo_n += 1 prune(hi, False) out = [] for i, x in enumerate(xs): if not lo or x <= -lo[0]: heapq.heappush(lo, -x); lo_n += 1 else: heapq.heappush(hi, x); hi_n += 1 rebalance() if i >= k: old = xs[i - k] dead[old] += 1 if old <= -lo[0]: lo_n -= 1 prune(lo, True) else: hi_n -= 1 prune(hi, False) rebalance() if i >= k - 1: out.append(-lo[0] if k % 2 else (-lo[0] + hi[0]) / 2) return out if __name__ == "__main__": print(sliding_median([1, 3, -1, -3, 5, 3, 6, 7], 3)) # [1, -1, -1, 3, 5, 6] print(sliding_median([1, 2, 3, 4], 2)) # [1.5, 2.5, 3.5]The lower heap holds the smaller half (negated, to make a max-heap) and the upper heap the larger half, with the lower heap allowed one extra element. The median is the top of the lower heap for odd k, or the mean of both tops for even k.
Removing the element that leaves the window is the hard part. Instead of searching the heap, the solution records it in a dead counter and decrements the live size of the heap it belongs to. Dead values are physically removed only when they reach the top of a heap, which is when they could affect the answer. Because live counts, not physical sizes, drive rebalancing, the halves stay balanced. Each element is pushed and popped a bounded number of times, so the total cost is O(n log n) in the worst case and O(n log k) when the heaps are pruned regularly.
The solution was checked against a brute-force sort of every window on two thousand random inputs with many duplicates. Interviewers often accept a sorted list with bisect, which is O(nk) because of insertion, then ask for the heap version.
- Limit order book with price-time priorityHard · C++
Implement a limit order book for one instrument.
addtakes an order with id, side, integer price in ticks and quantity, matches it against the opposite side by price-time priority, returns the trades (always at the resting order's price) and rests any remainder.cancel(id)removes a resting order in O(log P) or better, where P is the number of price levels. Reject duplicate ids and non-positive quantities.Hints
- One ordered map per side, keyed by price, each level a FIFO list.
- Keep a hash map from order id to its list iterator so cancel does not search.
- Trade at the maker's price, and erase empty levels.
Solution and explanation
#include <cstdint> #include <functional> #include <iterator> #include <list> #include <map> #include <unordered_map> #include <vector> enum class Side { Buy, Sell }; struct Order { uint64_t id; Side side; int64_t price; int64_t qty; }; struct Trade { uint64_t taker, maker; int64_t price, qty; }; // Limit order book with price-time priority. Prices are integer ticks. class OrderBook { using Level = std::list<Order>; std::map<int64_t, Level, std::greater<int64_t>> bids_; // best (highest) first std::map<int64_t, Level> asks_; // best (lowest) first struct Loc { Side side; int64_t price; Level::iterator it; }; std::unordered_map<uint64_t, Loc> index_; template <typename Book, typename Cross> void match(Order& o, Book& book, Cross crosses, std::vector<Trade>& out) { while (o.qty > 0 && !book.empty() && crosses(book.begin()->first)) { auto lvl = book.begin(); Level& q = lvl->second; while (o.qty > 0 && !q.empty()) { Order& m = q.front(); int64_t n = o.qty < m.qty ? o.qty : m.qty; out.push_back({o.id, m.id, lvl->first, n}); // trade at resting price o.qty -= n; m.qty -= n; if (m.qty == 0) { index_.erase(m.id); q.pop_front(); } } if (q.empty()) book.erase(lvl); } } template <typename Book> void rest(const Order& o, Book& book) { Level& q = book[o.price]; q.push_back(o); index_[o.id] = {o.side, o.price, std::prev(q.end())}; } public: // Returns the trades caused by this order; any remainder rests in the book. std::vector<Trade> add(Order o) { std::vector<Trade> out; if (o.qty <= 0 || index_.count(o.id)) return out; // reject bad or duplicate id if (o.side == Side::Buy) { match(o, asks_, [&](int64_t p) { return p <= o.price; }, out); if (o.qty > 0) rest(o, bids_); } else { match(o, bids_, [&](int64_t p) { return p >= o.price; }, out); if (o.qty > 0) rest(o, asks_); } return out; } bool cancel(uint64_t id) { auto f = index_.find(id); if (f == index_.end()) return false; Loc l = f->second; index_.erase(f); if (l.side == Side::Buy) erase_from(bids_, l); else erase_from(asks_, l); return true; } template <typename Book> void erase_from(Book& book, const Loc& l) { auto lvl = book.find(l.price); lvl->second.erase(l.it); if (lvl->second.empty()) book.erase(lvl); } int64_t best_bid() const { return bids_.empty() ? INT64_MIN : bids_.begin()->first; } int64_t best_ask() const { return asks_.empty() ? INT64_MAX : asks_.begin()->first; } };Bids are kept in a map sorted from high to low and asks from low to high, so the best price is always begin(). Each price level is a std::list of orders in arrival order, and a hash map stores, for every resting order, its side, price and list iterator. List iterators stay valid when other elements are removed, which makes cancel a hash lookup plus a map lookup, O(log P).
Matching takes the best opposite level while it crosses the incoming limit, fills against the front order, records a trade at the resting price, and removes orders and levels that reach zero. The remainder rests at the back of its level, which gives time priority.
The test harness checked a buy that sweeps two orders at 101 in time order and part of 102, cancels including a repeated cancel, a sell that trades at the better resting bid, and rejection of a duplicate id, under AddressSanitizer and UBSan. For a production book an interviewer will ask about replacing the map and list with arrays indexed by price offset and pooled order nodes, which avoids allocation and pointer chasing.
Design and debugging scenarios
Talk through each one out loud for 15–20 minutes, then compare your answer with the model walk-through.
- Design the tick-to-trade path for a market-making strategySystem design
You are building the production path for a market-making strategy on one exchange: it receives the exchange's multicast market data, maintains the book, decides quotes, runs risk checks and sends orders over the exchange's order entry protocol. Walk me through the design, the threads, the data structures and how you would keep tick-to-trade latency low and predictable.
Model walk-through
Requirements first. I would ask how many instruments, the peak message rate, the latency target (median and tail), whether the order protocol is FIX or a binary native protocol, and what risk limits apply. I would also ask whether the strategy logic changes often, because that decides how much flexibility the design must keep.
Data path. A feed handler thread on an isolated core busy-polls the network card through kernel bypass, arbitrates the A and B multicast lines by sequence number, decodes the binary messages and updates the book in place. The book for each instrument is an array of price levels indexed by offset from a reference price, with an order map for order-by-order feeds. On every update that changes the top levels, the handler calls the strategy directly in the same thread, because a queue hop adds latency and jitter. The strategy computes a fair value (for example a microprice with inventory skew), decides whether to move quotes, and passes order intents to the risk layer, which is a set of inline checks against precomputed limits and current exposure including orders in flight. The order encoder writes into preallocated buffers and sends on a TCP session, also via kernel bypass where available.
Off the hot path. Logging, metrics, persistence of order state and the FIX or native session administration run on other cores, fed by SPSC queues. The hot thread never allocates, never takes a lock and never makes a system call except the send.
Trade-offs. One thread for decode, strategy and send gives the lowest latency but limits how much work the strategy can do per message; heavier analytics run on another core and publish slower-moving parameters through a seqlock or a versioned shared structure. Templates rather than virtual calls keep the path inlined.
Failure modes. Packet loss on both lines: mark the book stale, pull quotes, recover from snapshot. Exchange disconnect: cancel-on-disconnect should be enabled at the venue, and the system must assume orders may still be live until confirmed. Runaway quoting: message rate limits and a kill switch independent of the strategy. Clock or configuration errors after a restart: start-up checks that refuse to trade.
Testing. Replay recorded captures through the same binary and compare decisions byte for byte with the previous version, inject gaps and reordering, and measure wire-to-wire latency with hardware timestamps under realistic burst rates.
- A junior researcher's mean-reversion backtest shows a Sharpe of 3.5Research
A junior researcher shows you a daily short-term reversal strategy on US small and mid cap stocks: buy the day's biggest losers and short the biggest winners at the close, hold one day. The backtest over ten years shows an annualised Sharpe ratio of 3.5 before costs and 2.1 after a flat 5 basis points per trade. What do you do?
Model walk-through
Frame. Short-term reversal is a well-known effect, so a positive gross result is plausible. A net Sharpe above 2 on small caps at one-day holding periods is not, so I treat the number as a hypothesis and look for the specific errors this design invites.
Timing and prices. The signal uses today's return to the close and trades at that same close. That assumes we know the closing price before trading at it. I would rerun with the signal computed from the return to a time before the close, or trade at the next day's open, and see how much survives. I would also check for bid-ask bounce: a closing print at the bid looks like a loss and then reverts as the next print is at the ask, creating fake reversal. Using mid quotes instead of last trade prices tests this directly.
Universe and data. Is the universe point-in-time, including delisted stocks with their delisting returns. Are there stale prices in illiquid names, which also create fake reversal. Are corporate actions adjusted correctly, since an unadjusted split looks like a huge loser that then recovers.
Costs. A flat 5 basis points is far too low for small caps traded fully every day. Turnover is near 200% per day for a one-day hold of a long-short book, so I would model half-spread per name from historical quotes plus square-root impact, and look at net Sharpe as a function of capital. Biggest movers are often the widest spreads.
Concentration and stability. Break returns down by year, by size bucket and by liquidity. If the result comes mostly from the smallest names or a few volatile years, capacity is tiny. Compare long and short legs: shorting the day's winners in small caps may be costly or impossible to borrow.
Process. Ask how many variants were tried (lookbacks, thresholds, universes). Then agree an out-of-sample test before anyone touches the data again.
Outcome. I would coach the researcher through these checks rather than dismiss the work. The realistic result is a smaller, capacity-limited effect that may still be useful as one input to a larger model.
- Review this research code for biasCode review
A colleague asks you to review this pandas snippet before they share results. The frame
dfhas one row per date and ticker with columns close and volume, and the ticker list came from today's index constituents.df = df.sort_values(['ticker', 'date']) df['ret'] = df.groupby('ticker')['close'].pct_change() df['ret'] = df['ret'].fillna(method='bfill') z = (df['ret'] - df['ret'].mean()) / df['ret'].std() df['signal'] = -z df['fwd'] = df.groupby('ticker')['ret'].shift(1) ic = df.groupby('date').apply(lambda g: g['signal'].corr(g['fwd'], method='spearman')) print(ic.mean())Model walk-through
Summary. The code has four look-ahead or selection problems and one statistical one. As written, the IC it prints is not evidence of anything. I would go through them in order of severity.
1. The forward return points backwards.
shift(1)moves yesterday's return onto today's row, so the signal (minus today's return) is compared with the past, not the future. The forward return should beshift(-1)within each ticker. As written, this measures the autocorrelation of returns rather than prediction, and it is easy to miss because the code runs and gives a plausible number.2. Backward fill leaks the future.
fillna(method='bfill')fills a missing return with the next available one, and because it runs over the whole column rather than per ticker, a run of missing values at the end of one ticker's history is filled from the next ticker's data. Missing first returns should simply stay missing and be dropped.3. Full-sample normalisation. The mean and standard deviation are computed over all dates and tickers, so every z-score uses future data. For a cross-sectional signal the normalisation should be per date, for example a rank or z-score within
groupby('date'). Since Spearman IC is rank-based, a global linear rescaling does not change the IC here, but the habit will bite the moment the same signal feeds a portfolio.4. Survivorship. Using today's index members for the whole history excludes companies that left the index, which biases results. The universe must be point-in-time membership.
5. Statistics. Printing only the mean IC hides whether it is consistent. I would add the standard deviation of daily IC, the t-statistic, the fraction of positive days, and results by year.
Smaller points.
fillna(method=...)is deprecated in recent pandas in favour ofbfill()andffill(); the groupby apply is slow on large panels; and there is no handling for prices that are zero or missing.How I would deliver it. I would write the review as concrete changes with a short reason each, and suggest a test: replace the forward returns with random numbers and confirm the IC goes to zero, which would have caught the first bug immediately.
- Live PnL is well below the backtestDebugging
An intraday strategy went live three weeks ago. Its paper-traded version, run on the same signals with the backtest's fill model, made money every week. The live version has lost money. The signals are identical in both. How do you find out why?
Model walk-through
Approach. The signals match, so the gap is in what happens between signal and fill, or in how PnL is measured. I would decompose the difference trade by trade rather than look at totals.
Step 1: check the accounting. Confirm both PnL numbers use the same prices, fees, rebates, borrow costs and marking times. Reconcile live positions with the broker or venue drop copy. A surprising number of live-versus-backtest gaps are fee schedules or exchange rebates that the backtest assumed and the account does not get.
Step 2: join intended and actual orders. For each signal, list the order the paper system would have sent and the order the live system did send, with timestamps. Differences in what was sent point to configuration, risk limits blocking orders, or rounding of sizes.
Step 3: compare fills. For the same orders, compare paper fills with live fills on fill rate, fill price and timing. Typical findings: passive orders that the simulator filled when the price touched the limit, but live orders were deep in the queue and did not fill; or live passive orders that filled mostly just before the price moved against them. That is adverse selection, and markouts at a few horizons after each fill will show it clearly.
Step 4: latency. Measure time from the market event that triggered each signal to the order reaching the venue. If the edge decays within milliseconds and live latency is much higher than the simulator assumed, the strategy is trading on stale information. Plot PnL per trade against latency.
Step 5: market impact and competition. If live orders are larger than in research, impact may be eating the edge. Check whether the effect weakened in the period in general, using the paper results across the same weeks.
Conclude and fix. Usually the answer is a fill model that was too generous for passive orders. The fix is to calibrate queue position and fill probability from the live fills, re-run the backtest with the calibrated model, and decide whether the strategy is still viable. Three weeks is also short, so I would compute the confidence interval on the gap before concluding the strategy is broken.
- Tail latency spikes in the order gatewayDebugging
Your order gateway's median latency is a few microseconds, but a few times a minute single orders take hundreds of microseconds. The spikes do not line up with market data bursts. How do you investigate?
Model walk-through
Measure precisely first. I would make sure the hot path records timestamps at each stage (receive intent, risk check, encode, send) into a preallocated buffer, so I can see which stage the spike is in. Spikes that are not caused by load usually come from the environment or from rare code paths, so the stage breakdown narrows it fast.
Environment causes. Check whether the thread is really pinned to an isolated core and whether anything else ran there: kernel threads, interrupts that were not moved, timer ticks. Tools that trace scheduling and interrupts on that core will show preemption. Check power management: a core that drops into a deep sleep state between orders takes time to wake, which fits a pattern of spikes after quiet gaps. Check for page faults, for example memory that was allocated but never touched until the first rare use, or memory not locked. Check for transparent huge page compaction and memory-management work in the kernel.
Code causes. Look for rare branches: a log statement that formats a string on a warning path, a container that grows past its reserved capacity, a hash map rehash when a new order id arrives, a periodic task such as statistics collection that runs on the hot thread, or a lock shared with a slow thread. A spike every few seconds with a regular period strongly suggests a timer-driven task.
Method. Correlate spike times with system events and with the code path taken. Reproduce in a lab by replaying the same order sequence with the same timing. Change one thing at a time and measure the 99.9th percentile again, because fixing the wrong thing can make the median better and the tail no different.
Typical fix and prevention. Common outcomes are an unmoved interrupt, a hash map rehash fixed by reserving capacity at start-up, or a logging call moved to a queue. Afterwards I would add a latency regression test that fails if the tail grows, and a start-up check that verifies core isolation and power settings.
Run a mock interview
Ask a friend to play the interviewer, or record yourself. Pick questions and an exercise you have not opened yet.
| Minutes | Stage | What to do |
|---|---|---|
| 0–5 | Introduction | Give a two-minute summary of one project: the problem, your role, the result in numbers and one thing that went wrong. Practise it until it fits without rushing. |
| 5–15 | Probability and mental maths | Have a friend read five questions from the first section, or draw them at random. Answer aloud without paper. Then do ten two-digit multiplications and three square roots against a timer. |
| 15–30 | Coding | Pick one exercise you have not looked at, in Python for research roles or C++ for systems roles. Talk through the approach for two minutes, write it, then test it with your own edge cases before saying you are done. |
| 30–40 | Statistics or microstructure deep dive | Answer three linked questions from one section, letting your partner push with the follow-up question each time. Practise saying when you are not sure and how you would find out. |
| 40–55 | Scenario | Take one scenario and work it on a whiteboard: ask clarifying questions, state assumptions, give a structured answer, and name at least two failure modes and how you would test for them. |
| 55–60 | Your questions and review | Ask two questions about how the team researches or deploys strategies. Then write down which answers were slow or vague and add them to your study list for the next session. |
Cheat sheet
Facts worth having at your fingertips on the day.
- Annualising
- Multiply daily volatility and daily Sharpe by the square root of 252, about 15.9, assuming independent returns.
- Sharpe t-stat
- The t-statistic of the mean is roughly annual Sharpe times the square root of the number of years.
- Fundamental law
- Information ratio is roughly IC times the square root of breadth, with breadth counting independent bets.
- Kelly fraction
- For a binary bet, f = p minus q over b; for a strategy, roughly expected excess return over variance.
- Beta
- Beta equals correlation times the volatility of the asset over the volatility of the benchmark.
- Hedge effectiveness
- The variance removed by the best linear hedge is correlation squared.
- Microprice
- (bid times ask size plus ask times bid size) divided by (bid size plus ask size).
- Square-root impact
- Impact per share scales with volatility times the square root of order size over daily volume.
- Implementation shortfall
- Execution price against the arrival price, plus the cost of any unfilled part.
- Turnover cost
- Net return is about gross return minus turnover times cost per unit traded.
- Look-ahead test
- Lag the signal by one extra period; if most performance vanishes, look for leakage.
- Walk-forward
- Train on the past, test on the next block, roll forward; purge overlapping labels and add an embargo.
- FIX order messages
- D new order, F cancel, G cancel/replace, 8 execution report, 9 cancel reject.
- FIX session messages
- A logon, 0 heartbeat, 1 test request, 2 resend request, 4 sequence reset, 5 logout.
- FIX key tags
- 11 ClOrdID, 41 OrigClOrdID, 34 MsgSeqNum, 39 OrdStatus, 150 ExecType, 9 BodyLength, 10 CheckSum.
- FIX checksum
- Sum of all bytes before the tag 10 field, modulo 256, written as three digits.
- Cache line
- 64 bytes on most current x86 and many Arm cores; pad shared counters to avoid false sharing.
- Memory latency
- A miss to main memory costs on the order of a hundred nanoseconds; an L1 hit a few cycles.
- SPSC ordering
- Write the slot, then release-store the index; the reader acquire-loads the index, then reads the slot.
- Feed recovery
- On a sequence gap mark the book stale, stop trading, recover from snapshot, replay buffered messages.
- Price-time priority
- Best price first, then earliest order at that price.
- VaR versus ES
- VaR is a loss threshold; expected shortfall is the average loss beyond it and is subadditive.
- Correlation bound
- For corr(A,B) and corr(B,C) both 0.9, corr(A,C) is at least 0.62.
- Expected rolls
- Six rolls to see a six; about 14.7 to see all six faces; 6 tosses for HH and 4 for HT.
- q evaluation
- q reads right to left with no operator precedence; wavg gives a weighted average and aj an as-of join.
Red flags that sink candidates
Accepting a great backtest at face value. Interviewers hand you a high Sharpe on purpose. Fix: list the checks you would run (timing, universe, costs, data errors, number of trials) before you discuss the result.
Freezing on a probability question. Silence looks worse than a wrong first step. Fix: state the setup, try a small case or a recursion aloud, and estimate the answer before computing it exactly.
Ignoring transaction costs and capacity. A signal that is profitable before costs is not a strategy. Fix: always mention turnover, spread, impact and how the result changes as capital grows.
Using random cross-validation on time series. It tells the interviewer you have not worked with financial data. Fix: describe walk-forward validation with purging and an embargo.
Talking about latency only as an average. Trading systems are judged on their tail. Fix: speak in percentiles, explain how you measure them and what causes spikes.
Writing C++ that allocates, locks or logs on the hot path without noticing. Fix: say explicitly what is preallocated, which thread does slow work, and how data crosses between threads.
Treating an order as dead because you sent a cancel. Fills and cancels race on the wire. Fix: describe a state machine that accepts fills in pending states and reconciles against the venue.
Overclaiming. Inventing numbers, claiming a strategy made a specific return without context, or bluffing about a tool you have not used. Fix: say what you know, what you would check, and how.
Giving investment opinions instead of method. Interviewers do not want market predictions. Fix: talk about how you would test an idea, not whether you think a stock will go up.
FAQ
What is the difference between a quant researcher and an algo trading developer interview?
Quant researcher loops lean on probability, statistics, research design and Python data work, with a backtest critique round. Algo trading developer loops lean on C++ or another systems language, data structures, concurrency, networking and microstructure, with a system design round. Both usually include probability and mental maths, and many firms mix the two, which is why this page covers both. Check the job description and ask the recruiter which rounds to expect.
Do I need to know finance before an algo trading interview?
For junior roles, deep finance knowledge is usually not required, but you should understand the basics on this page: how an order book works, what bid, ask and spread mean, order types, what a backtest is and why costs matter. Interviewers care more that you reason precisely and learn quickly. For senior roles, expect detailed questions on microstructure, execution and risk in the asset classes the team trades.
Which programming language should I prepare in for algo trading interviews?
Python for research roles, because research teams work in pandas and numpy and set data-manipulation problems. C++ for low-latency developer roles, with emphasis on memory, templates, concurrency and performance. Some firms use Java, Rust or OCaml for trading systems, but they test the same ideas. Pick one main language and be fluent; see Modern C++ Roadmap and Python Roadmap, and practise algorithm problems with Data Structures and Algorithms Roadmap.
How hard are the mental maths tests in quant interviews?
They usually involve fast arithmetic with two and three digit numbers, fractions, percentages and simple estimation, under time pressure and sometimes with many questions in a few minutes. The difficulty is speed and accuracy together. Practise daily for a few weeks: multiplication by splitting numbers, squares near round numbers, converting fractions to decimals, and estimating roots. It improves quickly with practice.
How do I talk about past trading or research projects without breaking confidentiality?
Describe the problem, the method and your contribution in general terms, and give results as relative improvements or qualitative outcomes rather than exact strategy details, parameters or returns. Interviewers respect this and will not ask you to reveal a previous employer's proprietary methods. Personal projects on public data are useful precisely because you can discuss them fully.