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

Source References

This lesson synthesizes and paraphrases concepts from the sources below. No copyrighted text, problem sets, or solutions are reproduced. Return to the originals for full depth.