Interview guides · Belvedere Trading
Belvedere Trading Software Engineer Interview Guide
Belvedere Trading software engineer prep on databases: indexes, query plans, and the anomalies an isolation level still permits.
The 60-second brief
A Software Engineer at Belvedere Trading plays a pivotal role in designing, developing, and maintaining software solutions that support the firm’s trading operations. This position is crucial as it directly impacts the efficiency of trading strategies and the overall performance of trading systems. As a Software Engineer, you will be involved in various projects that require innovative problem-solving and collaboration with traders and quants, ensuring that technology aligns seamlessly with trading objectives.
Scope in the operational half of the job when the seat carries on-call. Rollout, rollback and what you would put on a dashboard belong inside a design answer rather than after it, and an answer that never reaches them reads as someone who has built systems but not run them.
Focus areas
- Treat a send timeout as unknown, not failed
- Detect sequence gaps before pricing off a book
- Fold execution reports idempotently on venue exec id
Overview
The work you do as a Software Engineer at Belvedere Trading is not just about writing code; it involves understanding complex financial markets, optimizing algorithms, and utilizing cutting-edge technologies to enhance trading performance. You will engage in projects that span across algorithm development, data processing, and system architecture, requiring both technical expertise and an analytical mindset. This role is critical for driving the success of the trading desk and influencing how the firm adapts to changing market conditions.
The Belvedere Trading interview process
Belvedere Trading candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Coding Assessment
What this round decides is narrow: whether you can produce code that runs and is correct on inputs nobody showed you. An elegant solution that does not compile scores below a plain one that does, so write a correct brute force first, say out loud that you know its cost, and improve it with the working version still on screen. What separates strong answers is who finds the broken case. Trace your own code against an empty input, a single element, and duplicate keys before you say you are finished, because being told is far more expensive than noticing.
What to demonstrate
- Whether degenerate inputs get checked without being asked for: an empty collection, one element, every element equal, and the extreme value the input type allows
- Whether the complexity you state matches the code you actually wrote, including a sort or a copy sitting inside a loop
- Whether the finished answer is verified against the worked examples before you call it done, rather than assumed correct because the code reads correctly
How to prepare
- Take five problems you have already solved and, without running anything, write down what each returns for empty input, a single element, and all-duplicates. Then run them and count how many you predicted wrong.
- Drill the brute force as its own skill: on ten problems, write only the obviously-correct slow version and time how long it takes to get it passing. If that is more than a few minutes, that is what to practise, not the optimal version.
- Add a fixed last step before you submit anything, reading only the loop bounds and the initial value of each accumulator, which is where most off-by-one errors live
Phone Interview
Because the format is not fixed, the first job in the room is classification. Listen to the opening question and decide what it is: a probe into work you have already described, a fresh problem to solve now, or a conversation about how you operate. Each wants a different register, and the common failure is forcing a rehearsed structure onto a question that did not ask for it. Running a full design ritual on a ten-minute debugging question reads as not listening. When you cannot tell which it is, ask how long they want to spend and answer at that depth.
What to demonstrate
- Whether the shape of your answer matches the question, so a yes-or-no gets answered before it is justified and an open prompt gets a direction before a detour
- Whether you check how much depth is wanted instead of deciding for them, and whether you stop when the answer is complete rather than continuing until someone interrupts
- Whether you can be redirected in the middle of an answer without restarting it from the beginning
- Whether a question outside your experience gets an honest boundary followed by reasoning from what you do know, instead of a confident answer with nothing behind it
How to prepare
- Rehearse one project at three lengths, roughly thirty seconds, three minutes, and a full walkthrough at the depth of a design review, and practise switching between them when someone interrupts mid-telling
- Have someone ask you five questions of deliberately mixed type in one sitting without telling you the types, and score only whether you identified each one correctly before you started answering
- Draft the sentence you will use to check depth, along the lines of asking whether the short version is useful here or they want the detail, and use it in a real conversation this week so the day of the round is not its first outing
Onsite Interview
A day like this is several different games in a row, and the expensive mistake is carrying the previous one into the next room. Coding rewards narrow precision and finishing inside a timer. Design rewards breadth, stated assumptions and naming what you are deliberately not building. Behavioural rewards specificity about people and decisions. Candidates who over-engineer a coding problem they were supposed to finish, or who start sketching class hierarchies before anyone has agreed what the system has to do, are usually still playing the last round. Between rooms, name out loud which game the next one is.
What to demonstrate
- Whether the coding round ends with something that runs and has been traced against a degenerate input, rather than an extensible design that was never finished
- Whether a design discussion opens by agreeing on traffic shape, read-to-write ratio and what is allowed to be stale, instead of proceeding from an architecture you arrived with
- Whether a behavioural answer names a person, a disagreement and what you did about it, rather than describing the system the story happened inside
- Whether the opening habits still appear late in the day: restating the problem, asking for constraints, saying the plan before typing
How to prepare
- Book three mocks of different types back to back on one afternoon and ask each interviewer afterwards which round you answered in the wrong mode
- Write a three-line opening script per round type — coding: restate, name the approach and its cost, then type; design: ask for scale, read-write mix and what must not break; behavioural: name the person, the stakes and the decision — and run it off a card so the switch is mechanical rather than remembered
- Practise coding with a timer you do not extend, stopping when it stops, so the trained reflex is to finish a correct solution rather than to keep improving one
Where candidates lose points
Comparing timestamps from two different venues, or subtracting a venue timestamp from a local one and calling the result latency.
Each venue stamps on its own clock, at its own point in its own pipeline, at its own resolution, so a difference between two of them mixes elapsed time with clock offset and with unstated stamping conventions. Subtracting a venue timestamp from a local receipt timestamp yields network delay plus the offset between two clocks, and unless both are disciplined to a common source the offset can exceed the quantity being measured. Use one local clock for any latency you intend to act on, discipline it properly - PTP with hardware timestamping reaches sub-microsecond, while plain NTP over a LAN is typically sub-millisecond, three orders of magnitude too coarse here - and keep venue and receipt timestamps in separate columns so nobody is tempted to conflate them.
Representing prices and quantities as binary floating point.
IEEE-754 binary64 cannot represent 0.01 exactly, so a cost basis accumulated in doubles drifts, and an equality test against a tick boundary fails unpredictably. The fix is an integer at a fixed scale - a count of ticks, or a scaled integer at the instrument's price_scale - which also turns the tick-size check into a modulus rather than a tolerance comparison. The nuance that lets this trap survive code review: binary64 represents every integer exactly up to 2^53, about 9.0e15, so a scaled integer carried in a double is exact right up until it is not, and the first failure tends to be a large notional in a low-priced instrument, in production.
A queue or buffer with no bound
Every producer-consumer boundary needs a capacity and a policy for reaching it: block the producer, shed load, or drop the oldest entry. Unbounded buffering converts a temporary slowdown into memory exhaustion and hides the backpressure signal that would have revealed the consumer was falling behind.
Trusting input because it came from your own front end
Anything crossing a trust boundary is hostile: parameterise queries instead of building SQL by concatenation, validate against an allow-list rather than a deny-list, and bound the size of anything you allocate from a request. Raising this unprompted in an API or design question is a cheap and unusually strong signal.
Questions & practice
Write a function to find the shortest path in a graph.
Medium · data structures and algorithms
Approach
- Choose the data structure from the access pattern, not from familiarity.
- State the target complexity and say which constraint rules the naive version out.
- Walk one small example through your approach before writing the whole thing.
Follow-up
- What is the worst case, and how likely is it on real data?
- Which test case would catch an off-by-one here?
Implement a simple stock matching engine.
Medium · data structures and algorithms
Approach
- Restate the input: its shape, its size, and what is guaranteed about it.
- Choose the data structure from the access pattern, not from familiarity.
- Name the brute-force solution and its complexity before improving on it.
Follow-up
- How does this change if the input no longer fits in memory?
- Which test case would catch an off-by-one here?
Solve a problem involving data structures, such as a binary tree or a hash map.
Medium · data structures and algorithms
Approach
- Restate the input: its shape, its size, and what is guaranteed about it.
- Name the brute-force solution and its complexity before improving on it.
- Walk one small example through your approach before writing the whole thing.
Follow-up
- What is the worst case, and how likely is it on real data?
- How does this change if the input no longer fits in memory?
Choose a book structure for constant-time top-of-book reads
Medium · data structure choice · cache locality · order book
You maintain a price-level book for one instrument on one venue. Deltas arrive as (side, price, new_aggregate_qty), where price is an integer at the instrument's price_scale and tick_size is constant within the session; new_aggregate_qty of zero deletes the level. Sustained 10^5 to 10^6 deltas per second, over 90 percent landing within ten ticks of the touch, and top of book is read after every delta. Choose the representation, then give apply(delta) and best_bid()/best_ask() with their expected cost, their worst case, and the memory per instrument per side. Say what happens when the price leaves your band.
Approach
- Derive the structure from the access pattern rather than from the abstract operation set: reads are overwhelmingly of one element (the touch), writes cluster within ten ticks of it, and the price domain is a dense integer lattice because every valid price is a multiple of tick_size. That argues for a flat array of aggregate quantities indexed by (price - base_px) / tick_size, with cached best_bid_idx and best_ask_idx, over any comparison-based container.
- apply(delta) writes one slot in O(1). An insert at a price better than the current best updates the cached index in O(1) with no scan. A delete of a non-best level is O(1). Only a delete of the best level requires finding the next occupied level inward, which is where the worst case lives.
- Make that scan cheap by shadowing the array with a bitset, one bit per level, 64 levels per 64-bit word: the next occupied level is a find-first-set from the current index, so a 4096-tick band costs at most 64 word reads and typically one. Memory is 4096 x 8 bytes plus 512 bytes of bitset per side, about 32.5 KiB per side, 65 KiB per instrument.
- Price the alternative honestly. A sorted map is O(log L) per update with correct semantics and no band, but it allocates a node per new level on the decode path and chases pointers on every read; the disqualifier is the allocation and the cache misses, not the logarithm. Keep the flat array for the actively quoted set (1,000 instruments is about 65 MB) and a map for the long tail, because 10^5 instruments at 65 KiB each is 6.5 GB.
- Handle the band exit explicitly: when a price falls outside [base_px, base_px + width x tick], rebase by memmoving the live window and clearing the rest, an O(width) operation that must be counted and alarmed rather than hidden, since it is a latency spike correlated with fast markets.
- Tie it to the gap invariant: the book carries a staleness flag, and an unrecovered sequence gap sets it so best_bid()/best_ask() report unusable rather than returning the last well-formed state.
Worked solution (30 min)
- Set tick_size 1 at price_scale 2 and base_px 9990, giving index i = px - 9990 over a 32-level band; initialise bids at 9998 (qty 500) and 9999 (qty 300), asks at 10001 (qty 400) and 10002 (qty 700).
- Record best_bid_idx = 9, best_ask_idx = 11 and the two bitsets.
- Apply (bid, 9999, 0): clear slot 9, clear its bit, then find-first-set downward from index 8 to land on 9998.
- Apply (bid, 10000, 200): the price is better than the current best, so set slot 10 and update best_bid_idx with no scan.
- Apply (bid, 10000, 0) and (bid, 9998, 0) in turn and confirm the best walks down correctly, then delete the final bid and confirm the empty-side result.
- Instrument the scan to count word reads and run a 10^6-delta replay to get the distribution.
Expected result: After the first two deltas, best bid is 10000 at qty 200 and best ask is 10001 at qty 400, a one-tick spread. After the remaining deletes the bid side is empty and best_bid() returns an explicit empty sentinel, never a zero-quantity level. The scan-word histogram is dominated by 1 with a tail no longer than 32/64 = 1 word for this band.
Verify it yourself
- Every best-level read matches a brute-force scan of the whole band over a 10^6-delta replay.
- An insert at a price better than the current best never increments the scan counter.
- Emptying a side returns the empty sentinel rather than the last known price, and flipping the staleness flag makes both accessors refuse rather than answer.
Follow-up
- Move from price-level to order-level (each resting order tracked individually). What does that cost you, and what does queue-position modelling need from it?
- The venue sends a snapshot plus increments after a gap. Where do you buffer the increments, and at which sequence number do you start applying them?
- How do you verify this book against an independent full-depth snapshot mid-session without stalling the decode thread?
Current order state from an append-only event log
Easy · window-functions · indexing · event-sourcing
order_event is append-only: event_id BIGINT PRIMARY KEY, order_id BIGINT, strategy_run_id UUID, seq_no INT (1-based and contiguous per order, UNIQUE (order_id, seq_no)), status_after, cum_qty, leaves_qty, received_ts TIMESTAMPTZ(6). The table holds 10^8 rows across 10^7 orders. Write the query returning current status_after, cum_qty and leaves_qty for every order in a supplied list of order_ids, then the variant returning current state for all orders of one strategy_run_id. State the index each needs, and why ordering by received_ts instead of seq_no is wrong.
Approach
- For the id list use SELECT DISTINCT ON (order_id) ... ORDER BY order_id, seq_no DESC, which takes the first row of each group and stops. The portable equivalent, ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY seq_no DESC) = 1, ranks every row of every group before discarding all but one.
- Index (order_id, seq_no DESC). A btree can be scanned forwards or backwards as a whole, not per key, so the mixed-direction ORDER BY (order_id ASC, seq_no DESC) is not satisfied by (order_id, seq_no) and the planner inserts a sort.
- For the strategy_run_id variant the leading column changes to (strategy_run_id, order_id, seq_no DESC); otherwise the run's orders are found by a scan and grouped afterwards, which is the difference between 10^4 index descents and 10^8 row reads.
- Justify seq_no over received_ts concretely: received_ts is neither unique nor monotonic across reports. Two reports can share a microsecond, and a resend after a reconnect can arrive after a report it precedes, so MAX(received_ts) picks a non-deterministic row and can return an older state than the one the system acted on.
- Name the cost that motivates a maintained order_current table: this query is one index descent per order in scope, fine for a list and unacceptable for a dashboard refreshing every live order in a session. The trade-off is a second write on the order path and a crash window between the append and the update.
Follow-up
- How do you assert seq_no is contiguous per order, and what should the system do when it is not?
- Write the query listing orders whose latest row violates cum_qty + leaves_qty = order_qty while the order is live.
- How would you keep order_current correct if the writer can crash between appending the event and updating the summary row?
Denormalising instrument attributes onto the position snapshot
Medium · denormalization · read-patterns · temporal-joins
A risk dashboard reads every open position for a desk once a second, roughly 50,000 rows, and each row needs price_scale, contract_multiplier and currency from instrument_version resolved as of now, plus a mark price. Today that is a range-predicate join per row against an effective-dated table. Propose a model that serves this read, state exactly which columns you copy and what makes the copy safe, and say what happens to yesterday's snapshot rows when a vendor correction changes an instrument version that was already in effect.
Approach
- Characterise the read before choosing a shape: 50,000 rows a second, attributes immutable within a version, versions changing at most daily for almost every instrument. Be accurate about why the as-of join costs what it does, because the tempting explanation is wrong. The join carries instrument_id equality alongside the effective-dating predicate, so the planner hashes or merges on that key and applies effective_from <= now() AND (effective_to IS NULL OR effective_to > now()) as a filter qual; a range predicate does not prevent a hash join. The real costs are that the scanned side is the whole version history rather than the single row per instrument actually in effect, that two correlated inequalities are a selectivity the planner estimates badly, and that the answer is recomputed every second while being constant for the entire business date. On 50,000 positions against 150,000 stored versions the hash join discards two rows for every one it keeps, and the per-row nested loop a bad estimate would choose instead costs 50,000 index descents and roughly three times the runtime.
- Prefer the middle option to either extreme. Materialize an as-of dimension once per business date, one row per (instrument_id, business_date) resolved at session start, and make the dashboard an equality join on that pair. The range predicate disappears without duplicating attributes onto every position row.
- If you do copy onto position_snapshot, copy price_scale, contract_multiplier and currency together with their provenance: the effective_from (or a version surrogate) of the instrument_version they came from. A denormalised copy without provenance cannot be audited, cannot be reconciled and cannot be replayed.
- Handle the intraday hazard explicitly instead of hoping: a corporate action effective mid-session changes contract_multiplier within a business date, so a per-date dimension is wrong for that instrument that day. Either key the dimension at a finer grain for affected instruments or fall back to the as-of join for them, and know which instruments those are from the corporate-action feed rather than from a support ticket.
- Answer the correction question the books-and-records way: a derived snapshot records what the system believed when it computed, so a later vendor correction does not rewrite yesterday's rows. It produces a new snapshot row or a break, which is the same principle that keeps eod_derived and eod_reconciled as separate rows rather than one mutable number.
- State the cost you accepted: writes amplify, the dimension build becomes a session-start dependency, and a missed rebuild serves yesterday's attributes silently, so the dimension needs a freshness assertion that fails the dashboard rather than a scheduled job and optimism.
Worked solution (25 min)
- Measure the current as-of join with EXPLAIN (ANALYZE, BUFFERS) over 50,000 positions: record which join method the planner actually chose, how many version rows the scan discarded through the effective-dating filter, and the estimated-versus-actual rows on that filter.
- Build the per-business-date dimension with CREATE TABLE AS from the as-of query, and add UNIQUE (instrument_id, business_date).
- Re-run the dashboard query as an equality join against the dimension and compare timing, plan shape and the size of the side being scanned.
- Simulate a mid-session contract_multiplier change on one instrument and inspect what the dimension returns for it.
Expected result: The scanned side shrinks from every stored version to the one row per instrument in effect that date, the effective-dating filter quals and the misestimate that can flip the plan into a per-row nested loop both disappear, and the resolution stops being recomputed on every refresh. The mid-session contract_multiplier change is visibly wrong in the per-date dimension, which is the reason to handle those instruments separately rather than a reason to abandon the design.
Verify it yourself
- Dimension row count equals the number of instruments with a version in effect on that date.
- Every position row joins to exactly one dimension row; a LEFT JOIN producing NULLs means a missing instrument, not an empty desk.
- The dimension carries the effective_from it resolved from, and three spot-checked rows match instrument_version.
Follow-up
- The dashboard now wants 30 days of the same view. Does the per-date dimension still work, and what does the join key become?
- Where does mark_px live in your model, and what stops a stale mark from being indistinguishable from a fresh one?
- How large is the dimension, and does it fit in memory on the service that serves the dashboard?
How would you design a system to handle real-time trade data?
Medium
Approach
- Clarify what is being asked and what a complete answer contains.
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
If you identified a significant bug in the trading system just before a major release, what would you do?
Medium
Approach
- Work from the requirement backwards to the design.
- State your assumptions explicitly before working the problem.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Recover an order session that dropped mid-fill
Hard · consistency vs availability · idempotency · recovery · ambiguity
A venue session drops at 10:14:03 with three orders live: one submitted 8 ms earlier with no ack, one partially filled at 300 of 1,000, and one with a cancel request outstanding. The session returns 400 ms later and the venue replays execution reports it may already have delivered. An independent drop-copy stream is available. Design the recovery: what the gateway is entitled to believe about each order, what it may send while state is unknown, how replayed reports are applied, and when strategies may quote again. Name the trade-off you are making and what it costs.
Approach
- Classify by what is known rather than by the local status field. The unacked submit is ambiguous in both directions: the venue may hold it, may have filled it, may never have seen it. The partial fill has a known lower bound of 300 on cum_qty and an unknown upper bound. The outstanding cancel may have been applied or may have been beaten by a fill already matched.
- Resolve by asking, never by resending. On reconnect issue an order status request per live order, or a mass status request, and read the drop copy as an independent witness of the same facts. The client order id was persisted before the send precisely so the venue can be asked about it; resending the submit is the single action that converts the ambiguity into a real duplicate position.
- Apply the replayed reports through the same idempotent path as live ones: deduplicate on (venue_mic, venue_exec_id) and take the venue's cum_qty as authoritative over anything accumulated locally. Expect is_possible_duplicate to be set on some replayed reports and not others; the flag is a hint from the session layer, not the mechanism that makes reapplication safe.
- Make the consistency-versus-availability call and say what it costs. While any order's state is unknown the position is unknown, so every position-dependent risk check is evaluating a number that may be wrong. Permit risk-reducing actions, refuse new risk, and put a deadline on it: if status has not resolved within a stated number of seconds, escalate to a human and use the venue's cancel-on-disconnect if it offers one. The price is going dark for the 400 ms plus resolution time, and the thing bought is never hedging a position that does not exist.
- Close the loop afterwards. Reconcile the recovered state against the drop copy immediately and against clearing at end of day; a fill that appears only on the replayed stream and never in the status response is exactly the discrepancy position_snapshot's break column exists to surface rather than absorb.
Worked solution (35 min)
- Build a fake venue that acks, fills, disconnects on command, and on reconnect replays the last N reports with is_possible_duplicate set on some and not others.
- Drive all three scenarios and record the gateway's state before the drop, after reconnect, and after status resolution.
- Run the same scenario twice with the replayed reports delivered in different orders.
- Have the fake answer the status request for the ambiguous order two ways, unknown and filled, and follow both branches to the end.
Expected result: In every run the order's cum_qty after recovery equals the venue's stated value, never exceeds order_qty, and each order enters a terminal state exactly once. Replay ordering does not change the final state. The gateway sends no new order between the drop and status resolution and never resends the ambiguous submit. On the filled branch the position moves and the strategy is informed before it is permitted to quote again.
Verify it yourself
- Replaying the recorded session twice produces identical final state and an identical outbound message sequence, byte for byte.
- No outbound order message exists in the recording while any order was in an unknown state.
- Every execution applied during recovery appears exactly once in the position arithmetic, counted from the ledger rather than asserted.
Follow-up
- The status response says filled 500, the last live report said 300, and 300 has already been hedged. What is the sequence of actions now?
- Cancel-on-disconnect is available on this venue. What does turning it on cost you on a day with a two-second network blip?
- How do you test this without a venue, and what is the minimum fidelity the fake needs to be worth trusting?
Position fold consumer restarts on the same message
Easy · poison message · idempotency · dead letter queue
At 10:14 the position fold stopped advancing. The consumer crashes and restarts every 30 seconds on an identical stack trace, lag grows, and intraday positions are stale on every desk. The message at the head is an execution_report with exec_type='trade_correct' whose corrects_exec_id refers to an execution id the consumer has never seen. The consumer commits its offset only after a successful apply. Give an ordered checklist to confirm the diagnosis and a fix that neither drops the correction nor blocks the partition.
Approach
- Identify the exact message before theorising. Read the stuck offset and the message itself; a restart loop with an unchanging stack is the cheapest bug in the system to pin down and the one most often guessed at instead.
- Classify it. Malformed and never-processable is one class; well-formed but not yet resolvable is another. A trade_correct whose referent is absent is usually the second: the original execution may have arrived only on the drop copy, may belong to an earlier business date, or may simply be behind this message in another partition.
- Confirm by looking for the referent. Query execution_report on (venue_mic, venue_exec_id) for the corrects_exec_id across all three source values and the adjacent business dates. If it exists somewhere, the defect is a visibility or ordering assumption in the consumer, not the message.
- Clear the head-of-line block without losing the record. Park the message on a retry queue with bounded attempts and increasing delay, and on exhaustion move it to a dead letter queue that raises an alert - never a silent drop, because a correction is the venue restating a trade already booked into a position.
- Make redelivery cheap rather than rare. Apply is idempotent on (venue_mic, venue_exec_id) and the offset commits after apply, so reprocessing costs nothing; a correction is applied as a negating entry against the original rather than an edit, which is what the retained corrects_exec_id is for.
- Alert on the shape, not the symptom. Consumer lag alone says slow; lag with a flat committed offset and a rising restart count says poisoned, and those two want different responses from whoever is paged.
Follow-up
- The referent genuinely does not exist because the pipeline lost the original fill. What now, and what does the position row look like in the meantime?
- How many retries before the dead letter queue, and what makes that number defensible rather than arbitrary?
- Why is applying a correction by updating the original row the wrong shape for this table?
Suggested practice checklist
For someone fluent in a dynamic language who has shipped real work but has never had to say what the runtime is doing underneath. The week is built on measuring and deliberately breaking things, because the questions that expose this background are the ones where the interviewer asks why a second time.
Day 1 — Measure before reasoning
- Take a slow piece of your own code, write down in advance where you believe the time goes, then profile it and record how wrong the guess was. The cost is usually an allocation you did not notice or an accidental quadratic membership test.
- Replace one list membership test inside a loop with a set and measure at a thousand, ten thousand and a hundred thousand elements, confirming the shape of the curve rather than only that it got faster.
- Write down the three quantities you can now measure instead of assert: wall time, peak memory, and call count for the function you suspected.
Deliverable: A before-and-after profile of real code plus a written note on the size of the gap between the guess and the measurement.
Day 2 — References, copies, and the bugs they produce
- Write the function with a mutable default argument, call it three times, and explain the accumulating result: the default is evaluated once when the function is defined, so every call shares one object.
- Build a nested structure, take a shallow copy, mutate an inner element, and show that both views changed, because a shallow copy duplicates the container and not the elements. Then fix it with a deep copy and state the cost you just accepted.
- Write two functions, one mutating its argument in place and one rebinding the local name, and predict the caller's view of each before running it. That single distinction produces most of the bugs that pass their tests.
Deliverable: Three small programs whose output you predicted correctly before running, each with a one-line statement of the rule underneath.
Day 3 — Types, once, in a language that checks them
- Port one module you have already written, roughly a hundred lines, into a statically typed language, and record every place the compiler demanded an answer your original had left implicit: a value that can be absent, a numeric width, a case never handled.
- Write the same signature in both languages and state what the static one guarantees before the program runs and what it does not, since it will not save you from a wrong algorithm or an index out of range.
- Write the difference between an interface satisfied by declaration and one satisfied structurally, with one case each where the other approach would miss the mistake.
Deliverable: One module in two languages plus a list of the questions the type checker forced you to answer.
Day 4 — Concurrency, starting with what actually runs at the same time
- Run the same CPU-bound function across four threads and four processes and measure both. Under the default CPython build the threaded version will not speed up, because only one thread executes bytecode at a time; the process version will. Check which build you are on first, since free-threaded builds remove that lock and change the result.
- Then run a blocking I/O workload across four threads and measure it speeding up, because the interpreter releases that lock around blocking calls, which is why treating threads as useless is wrong as a general claim.
- Build the lost update: two threads each incrementing a shared counter a hundred thousand times, and show a final value below the expected sum, because an increment is a load, an add and a store and the thread can be suspended between them. Fix it with a lock and then measure what the lock costs.
Deliverable: Three measurements, threads against processes on CPU work, threads on I/O work, and a demonstrated lost update, each with the mechanism written underneath.
Day 5 — Debugging as a procedure rather than an instinct
- Work one real failure as a bisection: find a revision or an input size where it is good and one where it is bad, halve repeatedly, and state the two assumptions bisection needs, that the property changes exactly once across the range and that the test is reliable.
- Minimise one failing input to the smallest version that still fails, and record how many rounds it took.
- Keep a hypothesis log for one bug in three columns, what I believe, what would disprove it, what I observed, and stop yourself the first time you are about to change two things at once.
Deliverable: One bug worked to root cause with a written hypothesis log and a minimised reproducing input.
Day 6 — Tests that catch the bug you are about to write
- Implement an LRU cache with a capacity bound, then write the three test cases that would catch an off-by-one in eviction: insert exactly capacity items and assert nothing was evicted, insert one more and assert the least recently used key is the one gone, and read an old key just before that insert so the eviction victim changes.
- Add a property test comparing your implementation against a deliberately slow reference, an ordered list scanned linearly, over a few thousand random operation sequences, because a slow reference finds the cases you would not have thought to write.
- Write one numeric test that fails under exact equality and passes with a tolerance, and state why the tolerance has to be relative rather than absolute once the magnitudes grow.
Deliverable: An LRU implementation with three boundary tests, one property test against a slow reference, and one tolerance-based numeric test.
Day 7 — Debug something broken, out loud
- Have someone plant three defects in a two-hundred-line program, an off-by-one, a shared mutable state bug, and a wrong error-handling path, then find them while narrating, under a fixed rule: state the hypothesis before touching anything.
- Time each one and record which tool found it, reading, a printed value, a debugger, or a test, because the question asked in interviews is how you would find it rather than what it was.
- Write the sentence you will use when you do not yet know the cause, one that names the next measurement instead of offering a guess.
Deliverable: A recorded debugging session with time-to-find per defect and the method that found each.
The estimate and the day you knew
A slipped date is only a bad story if you sat on it. What matters is what you believed when you gave the number, the signal that told you it was wrong, how many days passed before you said so, and what you cut rather than asking for more time. Scope you defended counts as much as scope you dropped.
How do you handle pressure in a high-stakes situation?
Medium · behavioural and engineering judgement
Approach
- Pick a story where you made the decision, not one where you watched it.
- State the situation in two sentences and spend the rest on the reasoning.
- Close with what you would do differently, concretely.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
Describe a time when you had to work with a difficult team member. How did you handle it?
Medium · behavioural and engineering judgement
Approach
- Pick a story where you made the decision, not one where you watched it.
- Name the disagreement and how you resolved it with evidence.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
What does teamwork mean to you in a fast-paced environment like trading?
Medium · behavioural and engineering judgement
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Pick a story where you made the decision, not one where you watched it.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- What did you decide not to do, and why?
- What would you do differently if you ran that again?