CI/CD, Testing & UI — then the Season Finale
One evening, two parts. Part 1 makes your bot trustworthy: tests that pin its behaviour, a pipeline that refuses to merge red code, an image that runs identically anywhere, and a dashboard that tells you it is alive. Part 2 connects everything: three independent processes run as one market, the bugs that live in the seams come out, and the leaderboard locks on risk-adjusted return.
- Test at the right layer — and know which layer would have caught the bug that just cost you a session.
- Wire a pipeline that runs
pyteston every push and blocks the merge when it fails. - Read a live dashboard and say, in one glance, whether your bot is healthy or quietly broken.
- Trace the whole system — every arrow between Yahoo, broker, exchange, trader, dashboard and score.
- Survive partial failure — stale feeds, dropped sockets, rejected orders, duplicate fills.
- Defend your book with a tearsheet, and know why the steadiest curve wins the season.
Friday Dec 4 · the live block is the season finale: season weeks 9 and 10 back to back (25 + 35 minutes). Jump to Part 1 · CI/CD, Testing & UI or Part 2 · Integration & Demo.
CI/CD, Testing & UI
Proving it works, everywhere, every time — before the finale proves it for you.
Production engineering: proving it works, everywhere, every time
Three habits, one goal — your bot behaves the same on your laptop, in CI, and on the live arena at 6:05pm with the class watching the leaderboard.
A test is an executable claim about your code. pytest discovers every function named
test_*, runs it, and reports which claims still hold. That is all. The engineering judgement is not in
writing asserts — it is in choosing which claim to make at which layer.
The pyramid is that judgement made visible. Many unit tests at the base, each pinning one function in milliseconds with no I/O. Fewer integration tests, wiring two or three components together with fakes instead of the network. A handful of end-to-end runs that drive the whole path — bot to exchange to fill to P&L — and catch the wiring bugs nothing else can see, slowly and a little flakily.
Push logic down. A bug caught by a unit test costs you seconds; the same bug caught end-to-end costs a minute of CI; the same bug caught live costs you a session, in public.
# tests/test_signal.py
from strategy import momentum_signal
def test_momentum_goes_long_on_uptrend():
prices = [100, 101, 102, 103, 104]
sig = momentum_signal(prices, lookback=3)
assert sig.side == 'buy'
assert sig.qty > 0
def test_momentum_flat_when_no_trend():
assert momentum_signal([100, 100, 100], 3) is None
@pytest.mark.parametrize for the variations.A pipeline is a gate, and determinism is what makes it mean something
Continuous integration runs your suite on every push, on a clean machine that is not yours. Continuous delivery ships what passes. The merge stays blocked until the checks are green.
A GitHub Actions workflow is YAML in .github/workflows/. An event (on: [push])
triggers jobs; each job runs ordered steps on a fresh runner; a matrix fans the same job out across Python
versions. Nothing in it is clever — and that is the point. The runner has no internet by design, no cached
state, and none of your local environment. If your suite passes there, it passes anywhere.
on: [push, pull_request] # trigger
jobs:
test: # a job
runs-on: ubuntu-latest # the runner
strategy:
matrix:
python: ['3.11', '3.12'] # fan out
steps: # ordered
- uses: actions/checkout@v4
- run: make install
- run: pytest -q
Reproducibility is the other half. Same code plus same dependencies plus same OS equals same behaviour, so pin
exact versions (==, never >=), isolate in a venv, and freeze the whole environment in
an image. Order the Docker layers stable-to-volatile so the pip install layer stays cached and only
your code layer rebuilds.
# requirements.txt — pin exact versions websockets==12.0 pydantic==2.7.1 # Dockerfile — freeze the environment FROM python:3.11-slim WORKDIR /app COPY requirements.txt . # cached layer RUN pip install -r requirements.txt COPY . . # code layer CMD ["python", "-m", "team.trader"]
Live failures are public, and you cannot fix what you cannot see
A missing dependency, a hardcoded path, an untested edge case — any one of them crashes your bot mid-session while the dashboard shows the whole class your equity curve flatlining.
Shipping is not the end of the job. A running bot needs configuration you can change without a rebuild (ports, poll intervals — plain env vars), secrets that never enter git or the image (tokens injected at runtime), and observability: structured logs for what happened, metrics for what is happening now.
A dashboard is metrics made glanceable, and a trading dashboard has four non-negotiable panes. Positions — what do I own, and is it what I think I own. P&L — realized, unrealized, and the shape of the curve. Risk — how close am I to a limit, and what is my worst case right now. Connectivity — is the socket up, is the feed fresh, am I being throttled. Miss the fourth pane and you will confidently quote on a price that stopped updating four minutes ago.
The arena's own UI is exactly this: teacher/web_dashboard.py with MARKET (live books), FLOW (who
trades with whom), STATS and SEASON (per-team analytics and standings). The deploy kit behind it —
systemd units, Caddy for TLS, Docker for the environment — is how the hosted arena becomes
reproducible from bare metal up.
Five ways a tested bot still dies live
Each of these has cost a team a session. Each expands.
Tests that reach the network
yfinance is not a unit test: it is slow, it changes every
second, it fails on a plane, and it fails in CI where there is no internet by design. Worse, it passes locally, so
you only discover it after you push.
class FakeFeed: def price(self, sym): return 185.0) or
monkeypatch the call. Use tests/sim_session.py when you need a whole session.Tolerating a flaky test
Chasing the coverage number
Unpinned dependencies and "works on my machine"
requirements.txt says pydantic>=2. Three weeks later the runner
resolves a new minor version, a validator becomes stricter, and your bot cannot parse a message it has parsed all
season. The code did not change; the environment did.
A dashboard with no connectivity pane
Three questions
Instant feedback, no grading.
on_tick returns a buy signal when it should return None. Which test
layer should have caught it, and why that one?on_tick, assert the
Signal (or the None). Pushing this claim down to the base of the pyramid is what makes it
cheap enough to run on every push.OPEN. What is the first thing you check?Integration & Demo — Season Finale
Nothing new to build; everything to connect. Tonight the leaderboard locks.
Integration correctness is a different claim from unit correctness
Every part passing its own tests does not mean the system works. Bugs live in the seams between components that are each, individually, correct.
Unit correctness means each object honours its own contract in isolation — roughly 1,280 tests do that for the platform. Integration correctness means the objects honour each other's contracts when wired together, live, under load. The second does not follow from the first.
The classic seam bugs all have the same shape: two correct components disagree about timing or units. The signal fires faster than risk can veto it. A fill arrives before its ack. Two messages race on the same book. One side sends a price as a float, the other rounds to ticks — and both are right by their own spec.
In AlgoArena the seams are explicit, which is the whole reason the wire protocol is Pydantic. Three processes
speak only models from shared/messages.py; if a message round-trips, the seam holds. So test the
conversation, not just the speakers.
Live systems fail partially. Design for degradation.
A live system rarely dies all at once. The socket stays up while the data goes stale; the order is rejected while the process is healthy. Detect, do not assume.
Raw return is the most misleading number in finance
The same +12% can be skill or a coin flip that landed right. exchange/scoring.py ranks
return per unit of volatility, with a drawdown penalty — announced in Week 1, and it holds tonight.
A tearsheet is one page that answers two questions: did this make money, and how much risk did it take to do it? The equity curve tells the story before any number does — a smooth 20% beats a jagged 40%, because the second is one bad tick from a blow-up. Then the numbers: Sharpe (return per unit of risk), max drawdown (the worst pain an investor had to sit through), Calmar, turnover, exposure.
# exchange/scoring.py (score_equity) r = returns(equity) # per-snapshot returns vol = pstdev(r) # volatility of the book mdd = max_drawdown(equity) # worst peak-to-trough excess = mean(r) - RISK_FREE # above idle cash ratio = excess / max(vol, VOL_FLOOR) score = ratio * max(0, 1 - PENALTY*mdd) # PENALTY = 2.0 # ranked PER ROLE: traders vs traders, brokers vs brokers
Five ways to lose the finale with working code
None of these is a logic bug. Each expands.
Debugging live for the first time
EXCHANGE_HOST at 6:05pm spend the session reconnecting instead of trading, and the leaderboard does
not wait.
Crashing on ConnectionClosed
Retries that are not idempotent
Unbounded queues under back-pressure
on_tick processes them. The queue grows, latency grows
with it, and your bot is soon trading on a market that existed thirty seconds ago — while memory climbs.
Presenting the P&L instead of the process
Three questions
Instant feedback, no grading.
exchange/scoring.py rank them?ratio = excess / max(vol, VOL_FLOOR), then
score = ratio × max(0, 1 − 2·mdd). B's higher volatility shrinks the ratio and its 30% drawdown cuts
what is left by 60%. Same P&L, very different rank — the steadiest book wins, exactly as announced in Week 1.Demo day — and this week's work
Part 1 deliverables: a CI pipeline review, a Docker reproducibility sign-off, and your dashboard plan. Part 2 deliverables: the final integration checklist, a tearsheet from the season data, and a recorded demo rehearsal. Lock and tag your final branch — the leaderboard locks tonight.