Chapter 32: Quiz — Building a Platform from Scratch
Test your understanding of the concepts, code, and design decisions covered in this chapter. The quiz includes multiple choice, true/false, fill-in-the-blank, short answer, and code analysis questions.
Question 1 (Multiple Choice)
In our platform architecture, which layer is responsible for computing trade costs and updating share vectors?
A) Routers B) Services C) Engines D) Models
Show Answer
**C) Engines** The engines layer (order book engine, LMSR AMM) implements the core market mechanisms including price computation and trade execution. The services layer coordinates business logic and calls into the engines, while routers handle HTTP concerns and models define data structures.Question 2 (True/False)
In our order book implementation, a market sell order will always execute at the best bid price regardless of order size.
Show Answer
**False** A market sell order executes at the best bid price only if the best bid has sufficient quantity. If the market sell order is larger than the best bid, it will walk through multiple price levels, executing at progressively lower prices. This is called "walking the book" and results in price slippage.Question 3 (Fill in the Blank)
The LMSR cost function is $C(\mathbf{q}) = b \cdot \ln\!\left(\sum_{i=1}^{n} e^{q_i / b}\right)$. The cost of a trade that changes the share vector from $\mathbf{q}$ to $\mathbf{q'}$ is computed as ____.
Show Answer
$C(\mathbf{q'}) - C(\mathbf{q})$ The cost of any trade is the difference between the cost function evaluated at the new share state and the cost function at the previous state.Question 4 (Multiple Choice)
What is the maximum loss for an LMSR market maker operating a market with 5 outcomes and $b = 200$?
A) $200 \cdot \ln(2) \approx \$138.63$ B) $200 \cdot \ln(5) \approx \$321.89$ C) $200 \cdot 5 = \$1000.00$ D) $200 / \ln(5) \approx \$124.30$
Show Answer
**B) $200 \cdot \ln(5) \approx \$321.89$** The maximum loss for an LMSR market maker is $b \cdot \ln(n)$, where $n$ is the number of outcomes. With $b = 200$ and $n = 5$: $200 \times \ln(5) = 200 \times 1.6094 \approx 321.89$.Question 5 (Short Answer)
Explain what the "log-sum-exp trick" is and why it is necessary in our LMSR implementation.
Show Answer
The log-sum-exp trick is a numerical stability technique for computing $\ln(\sum_i e^{x_i})$. Instead of computing the exponentials directly (which can overflow when $x_i$ is large), we factor out the maximum value: $$\ln\!\left(\sum_i e^{x_i}\right) = x_{\max} + \ln\!\left(\sum_i e^{x_i - x_{\max}}\right)$$ Since $x_i - x_{\max} \leq 0$, all exponentials are in the range $[0, 1]$, preventing overflow. This is necessary because in our LMSR implementation, the values $q_i / b$ can become large when many shares have been traded, causing $e^{q_i/b}$ to exceed the maximum representable 64-bit floating-point number (approximately $1.8 \times 10^{308}$).Question 6 (Code Analysis)
What is the output of the following code?
amm = LMSRMarketMaker(b=100.0, num_outcomes=2)
cost1, _ = amm.execute_trade(0, 50)
cost2, _ = amm.execute_trade(0, 50)
print(cost1 < cost2)
Show Answer
**True** The first trade of 50 shares starts from equal prices (0.5 each), so the average price paid is relatively low. The second trade of 50 shares starts from a higher price for outcome 0 (because the first trade already moved the price up). Since the LMSR has increasing marginal cost for buying more of an already-popular outcome, `cost2 > cost1`, so `cost1 < cost2` is `True`.Question 7 (True/False)
In our platform, JWT tokens are stored in the database and validated against the stored copy on each request.
Show Answer
**False** JWT authentication is stateless. The token is validated by verifying its cryptographic signature using the server's secret key, not by looking it up in a database. This is one of the main advantages of JWT: no database query is needed for authentication. However, this also means that tokens cannot be individually revoked without maintaining a blacklist.Question 8 (Multiple Choice)
In our order book, when a buy limit order at $0.70 matches against a resting sell order at $0.65, at what price does the trade execute?
A) $0.65 (the ask price) B) $0.675 (the midpoint) C) $0.70 (the bid price) D) Depends on which order was placed first
Show Answer
**A) $0.65 (the ask price)** In our implementation, the trade executes at the **maker's price** (the resting order's price). Since the sell order at $0.65 was already in the book (the maker), and the buy order at $0.70 is the incoming order (the taker), the trade price is $0.65. This rewards liquidity providers by giving them the price they requested.Question 9 (Fill in the Blank)
In the LMSR, prices for all outcomes always sum to _ because the price function is equivalent to the _ function applied to the scaled share quantities.
Show Answer
Prices always sum to **1.0** because the price function is equivalent to the **softmax** function applied to the scaled share quantities $q_i / b$. The softmax function is defined as $\text{softmax}(x_i) = e^{x_i} / \sum_j e^{x_j}$, which by construction produces values that sum to 1.Question 10 (Short Answer)
Describe the three states in our market lifecycle and what transitions between them.
Show Answer
The three states are: 1. **Open**: The market is accepting orders and trades. Prices are updated dynamically. This is the initial state after market creation. 2. **Closed**: The market has passed its `closes_at` deadline. No new orders are accepted, but existing positions are preserved. The market awaits resolution. The transition from Open to Closed happens automatically when the current time passes `closes_at`. 3. **Resolved**: The market creator (or admin) has declared the winning outcome. Winning positions are paid out at $1 per share, and losing positions become worthless. The transition from Closed (or Open) to Resolved happens when the `resolve_market` endpoint is called. An additional implicit state is **Voided**, where the market is cancelled and all participants receive refunds based on their cost basis.Question 11 (Multiple Choice)
Which Python library does our platform use for password hashing?
A) hashlib B) bcrypt via passlib C) SHA-256 via hmac D) argon2
Show Answer
**B) bcrypt via passlib** We use `passlib.context.CryptContext(schemes=["bcrypt"])` for password hashing. Bcrypt is a deliberately slow hashing algorithm designed for passwords, which makes brute-force attacks computationally expensive. The `passlib` library provides a convenient wrapper that handles salt generation and hash verification.Question 12 (Code Analysis)
Consider this order book sequence. What is the final state of the book?
book = OrderBook()
book.submit_order(BookOrder(1, 100, 1, "buy", 0.50, 20))
book.submit_order(BookOrder(2, 200, 1, "buy", 0.55, 10))
book.submit_order(BookOrder(3, 300, 1, "sell", 0.52, 15))
Show Answer
Let us trace through: 1. Order 1: Buy 20 at $0.50. No asks exist, so it rests in the book. 2. Order 2: Buy 10 at $0.55. No asks exist, so it rests in the book. 3. Order 3: Sell 15 at $0.52. This is checked against bids. The best bid is $0.55 (Order 2, quantity 10). Since $0.55 >= $0.52, they match: - Trade: 10 shares at $0.55. Order 2 is fully filled. Order 3 has 5 remaining. - Next best bid is $0.50 (Order 1). Since $0.50 < $0.52, no match. Order 3's remaining 5 shares rest in the book as an ask at $0.52. **Final book state:** - Bids: Order 1 — Buy 20 at $0.50 - Asks: Order 3 — Sell 5 at $0.52 - Spread: $0.52 - $0.50 = $0.02Question 13 (True/False)
Our platform's order cancellation implementation immediately removes the order from the heap data structure.
Show Answer
**False** Our implementation uses **lazy deletion**. When an order is cancelled, its `remaining` quantity is set to zero, but the entry remains in the heap. The stale entry is skipped during future matching operations when the engine checks `if best_order.remaining <= 0`. This approach avoids the $O(n)$ cost of finding and removing an element from the heap.Question 14 (Short Answer)
Why do we use Float for financial values in the chapter's code, and what should be used in a production system instead?
Show Answer
We use `Float` (IEEE 754 double-precision floating-point) for simplicity and clarity in the instructional code. However, floating-point arithmetic introduces rounding errors that can accumulate over many transactions. For example, `0.1 + 0.2 = 0.30000000000000004` in floating-point. In a production system, you should use one of these alternatives: 1. **Decimal types**: Python's `decimal.Decimal` or SQLAlchemy's `Numeric` type with specified precision and scale (e.g., `Numeric(precision=18, scale=8)`). 2. **Integer representation**: Store values as integers representing the smallest unit (e.g., cents or basis points). A price of $0.5250 would be stored as the integer 5250 (in basis points). Either approach eliminates the rounding errors that floating-point introduces.Question 15 (Multiple Choice)
What HTTP status code should be returned when a user tries to place an order but has insufficient balance?
A) 401 Unauthorized B) 403 Forbidden C) 400 Bad Request D) 422 Unprocessable Entity
Show Answer
**C) 400 Bad Request** (as implemented in our code, which raises `ValueError` caught as a 400 error) or **D) 422 Unprocessable Entity** would also be acceptable. 400 indicates that the request is syntactically valid but cannot be processed due to a business logic error (insufficient funds). 422 is specifically for cases where the request body is well-formed but the contained instructions cannot be followed. 401 is for authentication failures and 403 is for authorization failures (the user is identified but not permitted), neither of which applies here.Question 16 (Fill in the Blank)
In FastAPI, the Depends() function implements the ____ pattern, which allows route handlers to declare their requirements (database sessions, authenticated users) without constructing them directly.
Show Answer
**Dependency injection** FastAPI's `Depends()` implements dependency injection. When a route handler declares `db: Session = Depends(get_db)`, FastAPI calls `get_db()` and passes the result as the `db` parameter. This decouples the handler from the specifics of how database sessions are created and managed.Question 17 (Code Analysis)
What will happen when this code runs?
amm = LMSRMarketMaker(b=100.0, num_outcomes=2)
amm.execute_trade(0, 1000)
price_0 = amm.price(0)
price_1 = amm.price(1)
print(f"{price_0:.6f}, {price_1:.6f}")
print(f"Sum: {price_0 + price_1:.6f}")
Show Answer
The output will be approximately:0.999955, 0.000045
Sum: 1.000000
After buying 1000 shares of outcome 0 with $b = 100$, the share vector is $\mathbf{q} = (1000, 0)$. The scaled values are $(10, 0)$. The price of outcome 0 is $e^{10} / (e^{10} + e^0) = e^{10} / (e^{10} + 1)$, which is very close to 1. The prices still sum to exactly 1.0 (within floating-point precision) thanks to the softmax formulation and the log-sum-exp trick for numerical stability.
Question 18 (True/False)
The LMSR automated market maker requires other traders to provide liquidity. Without opposing traders, it cannot offer prices.
Show Answer
**False** The LMSR is a subsidized market maker that always offers prices regardless of whether opposing traders exist. The market maker itself acts as the counterparty for every trade. This is one of the key advantages of the LMSR over order books: it provides guaranteed liquidity even in thin markets. The cost of this liquidity is the potential loss borne by the market maker operator, bounded by $b \cdot \ln(n)$.Question 19 (Short Answer)
Explain the difference between how our platform handles trades in an order book market versus an LMSR market. Who is the counterparty in each case?
Show Answer
**Order Book Market:** In an order book market, trades occur between two users. A buy order is matched against a sell order placed by another user. The counterparty is always another trader. The platform merely facilitates the matching. If no opposing orders exist, the trade cannot execute and rests in the book. **LMSR Market:** In an LMSR market, every trade is executed against the automated market maker itself. The AMM is the counterparty for every trade. The cost of a trade is computed using the cost function $C(\mathbf{q'}) - C(\mathbf{q})$, and the trade always executes immediately (no waiting for a matching order). The AMM's losses are subsidized by the market operator. In our code, the `TradingService` determines which path to take based on `market.market_type` and calls either `_execute_order_book_trade` or `_execute_lmsr_trade`.Question 20 (Multiple Choice)
When resolving a market, winning positions pay out $1 per share. If a trader bought 30 shares of the winning outcome at an average cost of $0.40 per share, what is their net profit?
A) $30 B) $18 C) $12 D) $0.60
Show Answer
**B) $18** The trader receives $1 per share for 30 shares = $30 total payout. Their total cost was 30 shares x $0.40 = $12. Net profit = $30 - $12 = $18. Alternatively: profit per share = $1 - $0.40 = $0.60. Total profit = $0.60 x 30 = $18.Question 21 (True/False)
In our implementation, when a market is voided, all participants receive back $1 per share regardless of which outcome they bet on.
Show Answer
**False** When a market is voided, participants receive a refund based on their **average cost basis**, not $1 per share. This means each participant gets back what they paid, not a fixed amount per share. This is implemented in the `void_market` method: `refund = position.shares * position.average_cost`.Question 22 (Code Analysis)
What is wrong with the following market creation request?
{
"title": "Rain?",
"description": "Will it rain tomorrow?",
"market_type": "lmsr",
"outcomes": ["Yes", "No", "Yes"],
"liquidity_parameter": 100.0,
"closes_at": "2030-01-01T00:00:00"
}
Show Answer
There are two validation errors: 1. **Title too short**: The `MarketCreate` schema requires `min_length=10` for the title. "Rain?" has only 5 characters and will be rejected by Pydantic validation. 2. **Duplicate outcomes**: The outcomes list contains "Yes" twice. The `outcomes_must_be_unique` validator in the `MarketCreate` schema checks that all outcome names are unique and will reject this with the error "Outcome names must be unique."Question 23 (Short Answer)
Our trading service uses in-memory dictionaries (_order_books and _lmsr_instances) to store engine state. Describe one specific failure mode this creates and how you would fix it.
Show Answer
**Failure mode: Server restart causes state loss.** If the FastAPI server process restarts (due to a crash, deployment, or scaling event), all in-memory order books and LMSR share vectors are lost. Open orders in the order book disappear, and LMSR prices reset to their initial uniform values, even though the database records show shares were traded. **Fix:** Persist engine state to a durable store. Options include: 1. **Reconstruct from database**: On startup, rebuild the LMSR share vectors from the `Outcome.shares_outstanding` column and replay open orders into the order book from the `orders` table. 2. **Use Redis**: Store the order book and LMSR state in Redis, which persists across process restarts and can be shared across multiple server instances. 3. **Event sourcing**: Store every trade as an event and rebuild engine state by replaying events on startup.Question 24 (Multiple Choice)
Which of the following is NOT a valid reason to prefer FastAPI over Flask for our prediction market platform?
A) FastAPI provides automatic request validation through Pydantic B) FastAPI generates interactive API documentation automatically C) FastAPI is inherently faster because it uses async/await D) FastAPI is the only Python framework that supports JWT authentication
Show Answer
**D) FastAPI is the only Python framework that supports JWT authentication** This is false. JWT authentication can be implemented in any Python web framework including Flask, Django, and Starlette. The JWT libraries (`python-jose`, `PyJWT`) are framework-agnostic. Options A, B, and C are all valid advantages of FastAPI: Pydantic integration (A), automatic OpenAPI docs at `/docs` (B), and native async support which can improve throughput for I/O-bound operations (C, though "inherently faster" is a simplification).Question 25 (Fill in the Blank)
In our order book, we use a _ data structure for the bid side (negating prices) and a _ data structure for the ask side, both implemented using Python's heapq module.
Show Answer
**max-heap** for the bid side and **min-heap** for the ask side. Since Python's `heapq` only provides a min-heap, we negate the prices for bids to simulate a max-heap. This ensures that the highest bid is always at the top (index 0) of the bids heap, and the lowest ask is always at the top of the asks heap. When we need the actual bid price, we negate the value again.Question 26 (True/False)
The CORS middleware configuration allow_origins=["*"] in our FastAPI app is appropriate for a production deployment.
Show Answer
**False** Setting `allow_origins=["*"]` allows any website to make API requests to our platform, which creates security risks including CSRF-like attacks from malicious websites. In production, this should be restricted to specific trusted domains (e.g., `allow_origins=["https://yourplatform.com"]`). The wildcard is only appropriate during development.Question 27 (Short Answer)
Explain the concept of "price-time priority" in order matching and why it is important for a fair market.
Show Answer
Price-time priority is the rule that determines the order in which resting orders are matched against incoming orders: 1. **Price priority**: Orders at better prices are matched first. For buy orders, higher prices have priority. For sell orders, lower prices have priority. This ensures that traders willing to pay more (or accept less) get served first. 2. **Time priority**: Among orders at the same price, earlier orders (those with earlier timestamps) are matched first. This follows a first-in-first-out (FIFO) principle. This is important for fairness because: - It rewards traders who offer better prices, encouraging tighter spreads and more efficient price discovery. - It rewards patience, since traders who post orders earlier get priority, incentivizing liquidity provision. - It is deterministic and transparent, so all participants know exactly how matching works.Question 28 (Code Analysis)
Given this LMSR configuration and trade sequence, what are the final prices?
amm = LMSRMarketMaker(b=100.0, num_outcomes=3)
amm.execute_trade(0, 50) # Buy 50 shares of outcome 0
amm.execute_trade(1, 50) # Buy 50 shares of outcome 1
# What are the final prices?
Show Answer
After the trades, the share vector is $\mathbf{q} = (50, 50, 0)$. The scaled values are $(0.5, 0.5, 0.0)$. Using the softmax formula: - $e^{0.5} \approx 1.6487$ - $e^{0.5} \approx 1.6487$ - $e^{0.0} = 1.0$ - Sum $= 1.6487 + 1.6487 + 1.0 = 4.2974$ Prices: - $p_0 = 1.6487 / 4.2974 \approx 0.3836$ - $p_1 = 1.6487 / 4.2974 \approx 0.3836$ - $p_2 = 1.0 / 4.2974 \approx 0.2328$ Outcomes 0 and 1 have equal prices (approximately 38.4%), while outcome 2, which had no shares bought, has a lower price (approximately 23.3%). The prices sum to 1.0.Question 29 (Multiple Choice)
When the get_current_user dependency raises an HTTPException with status 401, what does FastAPI do?
A) Returns a 500 Internal Server Error
B) Silently ignores the error and proceeds with current_user = None
C) Returns a 401 Unauthorized response with the error detail
D) Retries the authentication with a refreshed token
Show Answer
**C) Returns a 401 Unauthorized response with the error detail** FastAPI's exception handling catches `HTTPException` instances and converts them into the appropriate HTTP response. When `get_current_user` raises `HTTPException(status_code=401, detail="Could not validate credentials")`, FastAPI returns a JSON response with status 401 and the body `{"detail": "Could not validate credentials"}`. The `WWW-Authenticate: Bearer` header is also included as specified in our code.Question 30 (Short Answer)
Compare the order book and LMSR approaches for a prediction market with very few active traders (e.g., 5 users). Which mechanism would you recommend and why?