The Data Scientist role at Selby Jennings—particularly within the context of Equity Quantitative Research and Alternative Data—is a high-impact position situated at the intersection of finance, statistical modeling, and large-scale data analysis. You are not merely building models; you are distilling complex, often unstructured, financial and alternative datasets into actionable alpha-generating strategies.
This role is critical to the firm’s competitive edge. You will be expected to identify non-traditional signals that drive market movements, develop rigorous backtesting frameworks, and collaborate closely with stakeholders to refine investment hypotheses. The work is intellectually demanding, requiring a balance of academic-level research rigor and the pragmatic speed required by modern financial markets.
Expect the interview process to be rigorous, focusing on both your technical depth and your ability to articulate the commercial value of your research.
Preparation focus
editorialNo round sequence has been reported for this company, so work the categories below and confirm the format with your recruiter.
What to demonstrate
- Breadth across SQL, experimentation and product reasoning
- Ability to state assumptions before choosing a method
How to prepare
- Drill the practice exercises below and time yourself
- Prepare three quantified stories about decisions you drove
PracHub editorial advice for the preparation topics above.
Joining research panels to the current instrument master instead of its effective-dated version, so delisted, merged and bankrupt names silently disappear from the historical universe.
The names that leave a universe leave disproportionately after bad returns, so removing them raises backtested return and lowers backtested volatility at the same time. The bias is largest exactly where the strategy claims to add value, in the tails, and it is invisible in the output: the query succeeds, the row count looks plausible, and the equity curve simply looks better than it should. The fix is to resolve universe membership with a predicate on effective_from and effective_to, never on status = 'active'.
Modelling transaction cost as a constant number of basis points, independent of order size and volatility.
Temporary market impact scales approximately with volatility times the square root of participation, that is, of order quantity divided by average daily volume, so cost per share rises as size rises rather than staying flat. A constant-bps assumption is roughly right for the small orders used to calibrate it and badly wrong for the size the strategy would actually run, which is how a book that backtests well at modest notional loses money at ten times the size. It also makes capacity unmeasurable, because capacity is exactly the notional at which marginal impact equals marginal alpha.
Defining the cohort on a post-treatment condition
Ask how rows entered the table. Filtering on something that treatment itself influences, such as users who finished onboarding or accounts still active at ninety days, breaks comparability between arms; define the population at an entry point that precedes exposure and keep everyone in it.
Reaching for a model before the target metric exists
Before naming an algorithm, write down the label, the prediction time, and the action that changes when the score crosses a threshold. If you cannot say what decision the output drives, any modelling choice is guesswork dressed up as method.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
What is the most complex model you have built, and what was the busine…
What is the most complex model you have built, and what was the business outcome?
Approach
- Say how the offline result would be validated online before it is trusted.
- Pick an evaluation metric that matches the cost of each error type, not a default.
- Check what information would not exist at prediction time, and exclude it.
Follow-up
- What would you monitor after launch to know the model is still valid?
- Where could label leakage enter this setup?
Build a point-in-time as-of join without merge_asof
Two frames. sig: instrument_id (int64), knowledge_ts (tz-aware UTC, irregular, several rows per instrument per day), zscore_xs, about 7.5M rows across 5,000 instruments. bars: instrument_id, bar_close_ts (tz-aware UTC, one row per instrument per trading day), fwd_ret_5d, about 7.5M rows. For each bar row attach the most recent zscore_xs whose knowledge_ts is at or before that instrument's bar_close_ts, or NaN when the newest such score is more than 10 calendar days old. You may not use pd.merge_asof.
Approach
- Concatenate both frames with a source flag, sort by (instrument_id, ts, is_bar) so signal rows precede bar rows at an identical timestamp, then groupby('instrument_id')[[...]].ffill() and keep the bar rows. One sort and one forward fill, with no per-group Python.
- Carry the signal's knowledge_ts through the forward fill as its own column. It is the only way to apply the 10-day staleness cap afterwards, and it is the audit trail showing which score each bar actually used.
- The alternative is two searchsorteds. Factorize instrument_id to dense codes, compress both timestamp arrays to a shared integer rank, form key = code*(n_unique_ts+1) + rank so lexicographic order becomes numeric order, then np.searchsorted(sig_key, bar_key, side='right') - 1. Validate that the found row belongs to the same instrument, or the last row of one instrument is silently attached to the first bar of the next.
- Normalize timezones before sorting. A tz-naive column compared against a tz-aware one raises in current pandas and compared wrongly in older ones; convert both with dt.tz_convert('UTC') and assert the dtype rather than trusting the loader.
- Apply the cap as (bar_close_ts - carried_knowledge_ts) > Timedelta('10D') and set those to NaN. Report the NaN share by month: a rising share is usually a vendor outage or a delisted name still present in the bar file.
Follow-up
- The signal file contains rows with is_backfilled = TRUE, written after as_of_date by a rerun. Does your join still produce a point-in-time panel, and what would you change?
- How would you extend this to a forward-looking join, the first score strictly after the bar, and where in a research pipeline is that the correct thing to want?
- What changes when bar_close_ts and knowledge_ts can be exactly equal because the signal is computed off that same close?
Permutation test for mean IC with overlapping forward returns
panel has as_of_date, instrument_id, zscore_xs and fwd_ret_5d_excess, the five-trading-day forward return in excess of the universe's cap-weighted mean, for 252 dates and roughly 1,500 instruments a date. Compute the daily Spearman rank IC and its mean. Then build a permutation null from scratch and report a two-sided p-value. Compare it with a naive t-test on the 252 daily ICs and with a Newey-West t using four lags. State which null hypothesis your permutation scheme actually tests.
Approach
- Compute the daily IC by ranking both columns within as_of_date with method='average' and taking the Pearson correlation of the rank vectors. Decide explicitly how to treat dates where the universe shrinks, because an unweighted mean gives a 40-name date the same weight as a 1,500-name date.
- The obvious permutation shuffles zscore_xs within each as_of_date. That is the right exchangeability for the null 'the signal carries no cross-sectional ordering on any given day', and it preserves the daily sample size and the cross-sectional return structure, factor effects included.
- But it makes the permuted daily ICs independent across dates, and the real ones are not. A five-day forward return sampled every day shares four days with its neighbour, and the signal itself is persistent, so the observed IC series is strongly autocorrelated. Under an i.i.d. daily return null the variance of the mean of overlapping h-period observations inflates by a factor of h, so the within-day null is too narrow by about sqrt(5) and its p-value is anti-conservative.
- Fix it by permuting at the date level: circularly shift the whole signal panel's as_of_date labels by a random offset, keeping each day's cross-section and the signal's serial persistence intact while breaking its alignment with the returns. The 252 distinct shifts give an exact randomization test with a p-value floor of 1/252.
- Report all three numbers. The naive t and the Newey-West t with 4 lags (h-1 for h = 5) should differ by a factor near sqrt(5), and the shift-permutation p should agree with the Newey-West figure rather than the naive one. If it does not, one of the two is implemented wrongly and you have a cheap way to find out which.
Worked solution 45 min
- ranks = panel.groupby('as_of_date')[['zscore_xs','fwd_ret_5d_excess']].rank(method='average'); daily IC is the per-date Pearson correlation of the two rank columns.
- Within-day scheme: one rng.permutation of the signal rank vector inside each date, 10,000 replications of the mean IC.
- Shift scheme: remap as_of_date through a circular offset over the sorted unique dates, re-join to the returns on (shifted_date, instrument_id), and record how much of the panel drops because an instrument is absent on the shifted date, since a large loss biases the null.
- naive_t = mean(IC)/(std(IC, ddof=1)/sqrt(252)); Newey-West t with a Bartlett kernel and 4 lags.
- Put the three p-values, the two t-statistics, the replication count and the p-value floor in one table.
Follow-up
- With 252 circular shifts and the observed statistic the largest among them, what is the smallest p-value you can report, and is it small enough for the decision being made?
- How would you redo this on non-overlapping five-day samples, and how much power do you surrender?
- The signal has a 12-day decay half-life. Does that change your shift block length or your Newey-West lag choice?
Deduplicate a restated signal table into a leak-free panel
signal_score may hold several rows per (signal_id, instrument_id, as_of_date) because reruns rewrite history; each row carries model_version, knowledge_ts, computed_at, is_backfilled, zscore_xs, decile_rank, coverage_flag and universe_id. Build the panel a backtest may legitimately use on trading date D: for every instrument in universe_id = 'liquid_us_1500', the single most recent score whose knowledge_ts is at or before D 21:00 UTC, preferring a live row over a backfilled one. Return instrument_id, as_of_date, zscore_xs, decile_rank.
Approach
- Filter on
knowledge_ts, never onas_of_date.as_of_datesays what the score describes;knowledge_tssays the earliest instant every input was observable. Put it in the WHERE so it prunes before the window is evaluated. - Rank within the key:
ROW_NUMBER() OVER (PARTITION BY signal_id, instrument_id ORDER BY as_of_date DESC, is_backfilled ASC, knowledge_ts DESC, computed_at DESC)and keep rank = 1. In Postgres FALSE sorts before TRUE, sois_backfilled ASCis the live-row preference, written down rather than assumed. - Prefer ROW_NUMBER to a
MAX(computed_at)group-then-rejoin: the rejoin duplicates rows whenever two reruns share acomputed_at, which is exactly what a batch job produces. - Decide what
coverage_flag IN ('stale','imputed')means for this panel and encode it. Dropping those rows silently changes the universe size day to day, which surfaces later as an unexplained jump in measured IC rather than as a missing-data problem. - Express the cutoff as a parameter and the whole thing as a CTE so the backtest loop reuses one query text per date instead of a hand-edited copy.
Worked solution 25 min
- Write the filtered CTE:
universe_id = 'liquid_us_1500'andknowledge_ts <= :cutoff. - Add the ROW_NUMBER window with the four-key ordering and select rank = 1.
- Assert one row per instrument with a COUNT(*) versus COUNT(DISTINCT instrument_id) probe.
- Run the same query with
as_of_date <= Dsubstituted for the knowledge_ts filter and count how many rows differ. - Inspect the differing rows: they should be predominantly
is_backfilled = TRUEwithcomputed_atafter D.
Follow-up
- Write the query that proves the panel is leak-free. What would a violation look like in measured IC, and roughly how large would you expect the inflation to be?
- Produce every trading date in one pass instead of one query per date. What does that cost in plan shape?
- Two
model_versionvalues are live at once during a migration. How does the tie-break change, and who decides?
List fills that were never corrected, and survive the NULL
execution_fill records a cancel/correct as a new row with is_correction = TRUE and corrects_fill_id referencing the original; an ordinary fill has is_correction = FALSE and corrects_fill_id IS NULL. Return every original fill for a given settle_date that was never corrected, with fill_id, order_id, fill_qty, fill_px and commission_amt. A colleague's version filters WHERE fill_id NOT IN (SELECT corrects_fill_id FROM execution_fill) and returns zero rows on a day with thousands of fills. Explain why, then write the correct query.
Approach
- Name the mechanism precisely.
x NOT IN (...)expands tox <> v1 AND x <> v2 AND .... Against a NULL element the comparison is UNKNOWN, so the conjunction is either FALSE (when x matches some value) or UNKNOWN, never TRUE. Every probe row is filtered out. It is not an error, which is why it survives review. - Rewrite as
NOT EXISTS (SELECT 1 FROM execution_fill c WHERE c.is_correction AND c.corrects_fill_id = f.fill_id). This uses plain equality, is immune to NULLs in the inner column, and is the form the planner turns into a hash anti-join. - If
NOT INhas to stay for some reason, addWHERE corrects_fill_id IS NOT NULLto the subquery. It is correct, and it is fragile: the next nullable column someone probes reintroduces the identical bug with no new symptom. - Scope the correction lookup by time, not by the same
settle_date. A correction commonly arrives a day or more after the fill it amends, so restricting the inner query to the report date reports already-corrected fills as clean. - Close with arithmetic: originals on the date, minus distinct originals referenced by any correction row, must equal the returned count. If it does not, a correction references a fill_id that does not exist, which is a feed integrity issue worth raising.
Follow-up
- Would
LEFT JOIN ... WHERE c.fill_id IS NULLgive the same answer here? What changes if a single fill is corrected twice? - A correction lands three days after the TCA report went out. How do you make the report reproducible as-of its publication date rather than silently restating it?
These questions test your grasp of statistical modeling, financial the…
These questions test your grasp of statistical modeling, financial theory, and your familiarity with alternative data sources.
Approach
- State your assumptions explicitly before working the problem.
- Clarify what is being asked and what a complete answer would contain.
- Say what you would check first and why it is the highest-information step.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
How would you handle missing or noisy data in a high-frequency trading…
How would you handle missing or noisy data in a high-frequency trading dataset?
Approach
- Clarify what is being asked and what a complete answer would contain.
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Distinguish footprint adaptation from a durable execution improvement
A new child-order placement schedule has run six weeks in an order-level randomised test. The weekly treatment effect on implementation shortfall versus arrival, where negative is better, is -4.2, -2.8, -1.1, -0.3, +0.2 and +0.4 bps. Pooled over the six weeks the effect is -1.3 bps with a standard error of 0.5. The execution team wants to ship on the pooled result. Say what you would report to the trading committee, and what you would change in the design before any re-test.
Approach
- Refuse the pooled number as the answer. It averages a non-stationary effect, so it describes neither the launch week nor the steady state, and a t of 2.6 on it is a statement about the first two weeks. Test the trend directly: regress the weekly effect on week index with cluster-robust standard errors, or fit a level plus a break, and report the slope with its interval.
- Name the mechanisms and say what would separate them in the data. A genuinely mechanical improvement in placement should be flat in time. Adaptation to the algorithm's footprint predicts decay concentrated where the signature is most detectable, in high participation and low adv_20d names, and should show as a widening effective spread around the algorithm's own fills, visible from prevailing_bid_px and prevailing_ask_px in execution_fill.
- Check the primacy alternative before blaming the market. Parameters that season, or traders who stop overriding a new algorithm as they gain confidence in it, produce the opposite shape. Look at manual override rate, cancel_reason mix and the count of status = 'canceled' by week; a falling override rate explains a rising effect without any market participant doing anything.
- Estimate the asymptote rather than the mean. Fit effect_w = a + b * exp(-w / tau) over the six weekly points and report a with a bootstrap interval. If that interval covers zero, the honest statement is that the durable effect is not distinguishable from zero and the pooled -1.3 bps is an artefact of when you looked.
- Change the design before re-testing. Keep a permanent holdout of roughly 5 percent of flow so the effect stays measurable after launch, and pre-register a minimum steady-state window, here at least four weeks after the weekly effect stops trending, as the only basis for a ship decision.
Worked solution 25 min
- Plot the six weekly effects with their own standard errors and test the null of zero slope in effect ~ week.
- Fit effect_w = a + b * exp(-w / tau) and report the asymptote a with a bootstrap interval resampled over weeks.
- Compute the week 1-2 mean against the week 5-6 mean and test that difference.
- Re-cut the weekly series by participation quintile and adv_20d quintile to locate where the decay lives.
- Compute the power of the week 5-6 window on its own; if it cannot detect 1 bp, say so rather than reading its point estimate as a zero.
Follow-up
- If the decay were alpha decay in the underlying signal rather than adaptation by other participants, what would look different in this data?
- With a permanent holdout of only 5 percent of flow, what effect size can you still detect, and over what window?
- Is there a case in which you would ship this anyway, and what would you be committing to monitor?
Mean daily IC halved with no model change
The trailing-252-day mean rank IC for one signal_id fell from 0.031 to 0.016 over roughly six months. No model_version changed and no research code shipped. You have signal_score (signal_id, model_version, instrument_id, as_of_date, knowledge_ts, zscore_xs, universe_id, coverage_flag, is_backfilled, computed_at) joined to a daily forward-return panel keyed on instrument_id and date. Decide whether the forecast decayed or the input panel changed. Deliverable: an ordered checklist and the one query that settles it. You may not retrain the model.
Approach
- Stop looking at the trailing mean and recompute IC for each as_of_date. A 252-day moving average turns a single-day step into a decline that takes 252 days to complete, so the shape you were shown is equally consistent with a sudden break and with gradual decay. The first difference of the daily series locates a break to within a day.
- Count distinct instruments per as_of_date and per universe_id. A universe that silently shrinks changes the population the rank correlation runs over, which makes the new number a different estimator rather than the same one measured worse.
- Recompute the daily series restricted to coverage_flag = 'computed' and overlay it on the all-rows series. If the restricted series is flat across the break, the drop lives entirely in imputed or stale scores and the forecast is intact.
- Look at computed_at and the is_backfilled share around the break date. A live-job outage and a rerun that rewrote the same dates with different values produce the same IC drop and need different fixes.
- Only if all three cuts come back clean should decay be entertained, and then it has to be measured as decay: IC at horizons 1 through 21 and a half-life, not one number compared to another number.
Follow-up
- How many trading days of daily IC would you need to distinguish a true IC of 0.031 from 0.016 at conventional power, and what does that say about the six-month window you just used?
- If stale rows carry forward a score from one fixed prior date, what happens to their measured IC as the gap widens, and what does that do to the pooled number?
For someone who has spent the last year in notebooks, dashboards or modelling work and has not written raw SQL under time pressure. The first four days rebuild query fluency against a fixture you control and can verify by hand; the last three attach that fluency to the rest of the loop.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Build a fixture you can check answers against
- Create a local Postgres or SQLite database with four tables (users, sessions, events, orders) holding roughly 200 rows you generated yourself, so you know the contents well enough to predict every result.
- Deliberately seed the cases that break queries: a user with no sessions, a session with no events, two orders sharing a timestamp, a NULL in one join key, and one duplicated user row.
- Before writing any SQL, hand-compute five answers on paper (how many users placed at least one order, median orders per ordering user, and three others) and save them as the ground truth for the week.
Deliverable: A one-command seed script plus a text file of five hand-computed answers to grade every later query against.
Practice prompt ↗Practice prompt ↗Worked solution ↗02Joins, filters and NULL semantics
- Answer "which users have no orders" three ways (LEFT JOIN with IS NULL, NOT EXISTS, NOT IN) and confirm that the NOT IN version returns zero rows once the subquery contains a NULL, because the comparison is never TRUE.
- Reproduce the LEFT JOIN that silently collapses to an inner join by putting a right-table predicate in WHERE, then fix it by moving the predicate into the ON clause, and record both row counts.
- Create a fan-out bug on purpose by joining orders to order_items and summing the order total, then correct it with a pre-aggregated subquery and explain in one line which table changed the grain.
Deliverable: One annotated .sql file holding the three join traps, each with the wrong result and the corrected result side by side.
Practice prompt ↗Practice prompt ↗03Window functions and frames
- Write three window queries against the fixture: a running order total per user, the rank of each order within its user by value, and the day gap to that user's previous order, then check each against the day-one ground truth.
- Run ROW_NUMBER, RANK and DENSE_RANK over a column containing ties, print all three side by side, and write one sentence on when each is the correct choice.
- Switch one query from the default frame (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, which is what you get when ORDER BY is present and no frame is written) to ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, and explain why the output differs only when the ORDER BY column has duplicates.
Deliverable: Three verified window queries plus a short note explaining the RANGE versus ROWS difference in your own words.
Practice prompt ↗Practice prompt ↗04The four analytical query patterns
- Write a monthly retention grid: first order month per user, then months-since-first as the column, and verify that month zero equals the cohort size exactly.
- Sessionize the events table under a 30-minute inactivity rule using LAG plus a cumulative sum over a new-session flag.
- Build a four-step funnel that counts distinct users rather than events at each step, and state the rule you applied to a user who reaches step three without ever logging step two.
Deliverable: One file with the retention, sessionization and funnel patterns, each carrying a one-line note on the assumption it bakes in.
Practice prompt ↗Practice prompt ↗Worked solution ↗05Write SQL the way you will have to write it live
- Set a 12-minute timer and solve three medium prompts in a plain editor with no execution and no autocomplete, then run them and tally syntax errors separately from logic errors.
- Narrate one solution aloud while writing it, stating the grain of each intermediate result (one row per user, one row per user-day) before you type its body.
- Rewrite your slowest solution as a CTE chain where every CTE name states its grain, and time yourself re-solving it from blank.
Deliverable: A recording of one narrated solution plus an error tally that separates syntax from logic.
Practice prompt ↗Practice prompt ↗06One day for everything that is not SQL
- Write the preconditions of the two-sample t-test from memory, then check them: independent observations, and a difference in means whose sampling distribution is approximately normal, which at large sample sizes follows from the central limit theorem rather than from normality of the raw values.
- Write the difference between an odds ratio from logistic regression and a relative risk, and state the condition under which the two are close (low outcome prevalence).
- Prepare a 90-second answer to "how would you know this model is any good" that names the metric, the baseline you would beat, and the cost of the errors you care about.
Deliverable: One page of notes covering test preconditions, the odds-ratio caveat and the model-quality answer.
Practice prompt ↗07Full loop rehearsal
- Run a 45-minute mock with someone willing to interrupt: 20 minutes of SQL, 15 minutes defining a metric, 10 minutes on a past project.
- Re-solve from blank the two queries you were slowest on this week and compare the times against day five.
- Write a five-line answer to "walk me through a project" that puts a number in the first sentence and names the decision the work changed.
Deliverable: Mock feedback notes plus a timed project narrative you can deliver without reading it.
Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
Data people depend on systems owned by other teams, and much of the job is negotiating for instrumentation, access, or a fix to a broken pipeline. Prepare an example of getting something changed upstream that you did not control. Describe what you asked for, what you traded, and how you worked while you waited.
Why are you looking to transition into this specific area of quantitat…
Why are you looking to transition into this specific area of quantitative finance?
Approach
- State the situation in two sentences and spend the rest on your reasoning.
- Close with what you would do differently, concretely.
- Name the disagreement or constraint, and how you resolved it with evidence.
Follow-up
- How did you know the outcome was caused by your change?
- What did you decide not to do, and why?
Tell me about yourself and your background in quantitative research.
Tell me about yourself and your background in quantitative research.
Approach
- State the situation in two sentences and spend the rest on your reasoning.
- Close with what you would do differently, concretely.
- Quantify the outcome, including what you would not claim credit for.
Follow-up
- What would you do differently if you ran that project again?
- What did you decide not to do, and why?
How do you handle situations where your research results contradict th…
How do you handle situations where your research results contradict the prevailing market sentiment?
Approach
- State the situation in two sentences and spend the rest on your reasoning.
- Quantify the outcome, including what you would not claim credit for.
- Pick a story where you drove the decision, not one where you observed it.
Follow-up
- What did you decide not to do, and why?
- What would you do differently if you ran that project again?
- 01
Why are you looking to transition into this specific area of quantitative finance?
- 02
Tell me about yourself and your background in quantitative research.
- 03
How do you handle situations where your research results contradict the prevailing market sentiment?
Is this an official Selby Jennings interview guide?
No. It is PracHub's own research and practice material for the Data Scientist role at Selby Jennings. Rounds and questions reflect what candidates have reported, not a process Selby Jennings has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗Is the interview process mostly technical or behavioral?
It is heavily weighted toward the technical. While you must be a good cultural fit, your ability to solve problems and explain your research is the primary driver of success.
PracHub interview research ↗How can I stand out during the interview?
Demonstrate a deep, nuanced understanding of the datasets you have worked with in the past. Being able to discuss the "failures" or "noise" in your previous projects shows maturity and a realistic approach to research.
PracHub interview research ↗What is the typical timeline for an offer?
The process can move very quickly. If you perform well in the technical rounds, you could receive an update within a few days, though it depends on the specific team's hiring urgency.
PracHub interview research ↗Sources & methodology 3 sources ↗
No official company page is cited. Rounds and questions come from candidate reports and PracHub editorial material; each source shows the date it was read.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
PracHub page · Accessed 2026-09-22 - 02PracHub Data Scientist practice ↗
Cross-company practice questions for this role.
PracHub page · Accessed 2026-09-22 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
PracHub page · Accessed 2026-09-22