Phase 17 - Lesson 17.4

Software Design: DRY, Orthogonality, and Modularity

The Pragmatic Programmer’s core habits - don’t repeat knowledge, decouple components, and shoot tracer bullets - applied to a research codebase.

⏱ 55 min● Intermediate🔗 Prereqs: 17.1
↖ Phase 17 hub
Builds on: 17.1–17.3 made code fast, correct, and reproducible; now you make it maintainable.
Leads to: The Phase 18 capstones require a modular, DRY architecture; this lesson is the design rubric.

Learning Objectives

Click a status chip to cycle: Not started → In progress → Studied → Practiced → Needs review → Mastered.

Key Vocabulary

DRY (Don’t Repeat Yourself)
Every piece of knowledge has a single, authoritative representation; duplication of knowledge, not just code, is the enemy.
Orthogonality
Components are independent: changing one has no unintended effect on another; effects are ‘at right angles’.
Coupling
The degree to which one module depends on the internals of another; tight coupling makes changes ripple.
Tracer bullet
A thin end-to-end skeleton of the real system, built early to hit the target and then fleshed out.
Prototype
Throwaway code built to answer one question (is this feasible?), then discarded - unlike a tracer bullet.
Single source of truth
The one place a fact lives (a constant, a config, a function) so a change is made once and propagates.

Intuition & Motivation

Intuition
Two forces destroy research codebases. The first is duplication: the trading-day count 252 is typed in nine files, and one day you change eight of them. The second is coupling: your backtester reaches inside the pricer’s internals, so improving the pricer breaks the backtest. The Pragmatic Programmer’s antidotes are DRY (one home for each fact) and orthogonality (components change independently).

A third idea governs how you build: fire a tracer bullet - a thin, working end-to-end path (data → signal → backtest → report) - before perfecting any piece. You see the whole system integrate early, and each component then grows in place. A prototype is different: throwaway code to answer one risky question.

DRY: don’t repeat knowledge

DRY is often misread as ‘never copy-paste code’. The real rule is deeper: every piece of knowledge should have one authoritative representation. If the annualization factor \(252\), the risk-free curve, or the definition of ‘a trade’ appears in many places, a change must be made in all of them - and you will miss one.

Worked Example - Refactoring duplicated knowledge to a single source
1
Symptom: sharpe = mean/std*np.sqrt(252) appears in the momentum module, the mean-reversion module, and the report.
2
The knowledge ‘there are 252 trading days’ and ‘this is how we annualize’ is duplicated three times.
3
Refactor: one function def annualized_sharpe(r, periods=252): return r.mean()/r.std(ddof=1)*np.sqrt(periods).
4
Now every caller imports it. Changing to 260 days, or to an excess-return Sharpe, is a one-line edit in one place.
5
Verify: assert the refactored function reproduces the old inline results on a fixed dataset before deleting the copies.

Orthogonality: independent components

Two components are orthogonal if changing one does not force a change in the other. Draw the dependency arrows: if your signal generator imports the broker API, then swapping brokers touches your signals - a non-orthogonal design. The fix is an interface: components depend on a small contract, not on each other’s internals.

Design smellConsequenceOrthogonal fix
Backtester reads pricer’s private arraysImproving the pricer breaks the backtestDepend on a price(option) interface only
Data loader hard-codes one vendor’s columnsNew data source rewrites the strategyA loader returns a standard schema; strategy is source-agnostic
Costs baked into the signalCan’t test the signal without a cost modelSignal returns positions; a separate module applies costs
Key Idea
A good test of orthogonality: ‘If I change X, how many files must I touch?’ If the answer is ‘one’, the design is decoupled. If it’s ‘I’m not sure’, the coupling will bite you.

Tracer bullets vs. prototypes

A tracer bullet is a minimal but real end-to-end system: it loads a tiny dataset, computes a trivial signal, runs a one-line backtest, and prints a metric. It is production code, just skeletal - you keep it and grow it. A prototype is throwaway: you might mock the whole data layer in a notebook just to check whether an idea has any edge, then delete it. Confusing the two - shipping the prototype - is how research code rots.

\[\text{tracer bullet: thin but real, kept} \quad\neq\quad \text{prototype: mock, disposable}.\] (17.5)

Modularity and the DRY–orthogonality link

Modularity operationalizes both principles: small modules with clear interfaces give each fact one home (DRY) and let each piece change independently (orthogonality). A research repo typically factors into: data (load/clean), signals (features → positions), execution (positions + costs → fills), backtest (fills → P&L), and report (P&L → metrics/figures). Each depends only on the interface below it.

Common Mistakes to Avoid
  • Copy-pasting a formula into a new module ‘just for now’ - the copies drift and one gets fixed but not the others.
  • Treating DRY as ‘no duplicated lines’; two functions that happen to share a line but encode different knowledge should stay separate.
  • Coupling the signal to the broker or the data vendor, so a swap ripples through unrelated code.
  • Shipping a prototype as production; its mocks and shortcuts become load-bearing.
  • Over-abstracting too early - building a plugin framework before you have two real cases (a different failure than duplication).
  • Global config read directly everywhere, so no single component owns a parameter - a hidden form of coupling.
Quant Practitioner Tips
  • Ask ‘where does this fact live?’ Every constant (252, the cost model, the universe) should have exactly one home.
  • Refactor under test: pin behavior with an assertion, extract the function, confirm the assertion still passes, then delete copies.
  • Design interfaces as verbs: load(), signal(), execute(), evaluate(). Swap implementations freely.
  • Fire a tracer bullet on day one: get end-to-end plumbing working on toy data before optimizing any stage.
  • Prefer two concrete cases before you abstract; premature abstraction couples things that shouldn’t be.

Interactive: remove the duplication

The starter has the same Sharpe knowledge encoded three times. Refactor to a single source of truth and prove the refactor changed no numbers.

Knowledge Check

Q1 Easy
The DRY principle is best stated as:
Never write the same line of code twice
Every piece of knowledge has a single, authoritative representation
Always use functions
Comment everything
Q2 Medium
Your signal module imports the broker API, so switching brokers changes your signals. This violates:
DRY
Orthogonality (the components are coupled)
Reproducibility
Vectorization
Q3 Medium
A tracer bullet differs from a prototype in that it is:
Written in C++
Thin but real end-to-end code you keep and grow, whereas a prototype is throwaway
Always faster
Only used for testing

Practical Exercise

You are given a 900-line research script that loads data, computes three signals, applies costs, backtests, and plots - all inline, with the universe list and cost rate hard-coded in five places each. Propose a modular, DRY, orthogonal redesign: name the modules, their interfaces, and where each fact lives. Then describe how you would refactor safely.

▶ Show full solution

Modules and interfaces (each depends only on the one below):

  • data.load(universe, dates) → DataFrame with a standard schema; the vendor detail lives here only.
  • signals.build(prices) → positions; each signal is a function with the same signature so they are interchangeable.
  • execution.apply_costs(positions, cost_model) → net_returns; the cost rate lives in cost_model alone.
  • backtest.run(net_returns) → equity, metrics; knows nothing about how signals were built.
  • report.figure(metrics); pure presentation.

Single sources of truth: the universe list and the cost rate each live in one config object, imported everywhere; the annualization factor lives in one metrics function.

Safe refactor: first pin the script’s current outputs (equity curve, metrics) on a fixed seed/dataset with np.allclose assertions. Extract one module at a time, re-running the assertions after each extraction. Only when all pass and duplication is gone do you delete the original inline code.

After the reveal, answer for yourself: Which coupling in the original script would have hurt most six months later, and why?

Lesson Summary

Maintainable research code follows three Pragmatic Programmer habits. DRY: every fact (252, the cost model, ‘a trade’) has one authoritative home. Orthogonality: components depend on thin interfaces, so a change touches one file, not many. And you build by firing a tracer bullet - thin-but-real end-to-end code you keep - rather than shipping a throwaway prototype. Refactor under test so behavior is provably unchanged.

Retrieval Practice

Close the lesson and answer from memory before checking. This is deliberate, effortful recall - the single highest-yield study action.

▶ Show retrieval prompts & answers
Q: State DRY precisely and give a quant example of a knowledge duplication.
A: DRY: every piece of knowledge has a single authoritative representation. Example: the 252 trading-day annualization factor typed into many modules; it should live in one metrics function so a change is made once.
Q: What is orthogonality, and what is a quick test for it?
A: Orthogonality means components are independent, so changing one has no unintended effect on another. Quick test: ‘if I change X, how many files must I touch?’ A decoupled design answers ‘one’.

Completion Checklist

Confidence / mastery rating
Personal notes