Design Philosophy
Why tzutrader Exists
Most backtesting libraries fall into two camps: either they're huge frameworks trying to do everything, or they're simple scripts that get rewritten for each project. tzutrader explores a middle path—composable building blocks you can understand and extend.
This is an experiment in architecture. The goal isn't to build the most feature-complete backtesting platform. It's to explore patterns for composable, streaming financial data processing.
Core Principles
1. Composability
Components should mix and match like LEGO blocks. Any indicator works with any strategy. Any strategy works with any portfolio. This modularity lets you:
- Test different combinations quickly
- Replace one component without rewriting everything
- Build complex behavior from simple pieces
- Understand each part in isolation
Example:
// These all work together
SMA sma(20);
RSI rsi(14);
SMACrossover strategy(10, 30);
BasicPortfolio portfolio(100000, 0.001, 0.1, 0.2);
// Easy to swap components
EMA ema(20); // Drop-in replacement for SMA
MACDStrat different_strategy(12, 26, 9); // Different strategy, same interface
This is harder to achieve than it sounds. It requires careful interface design and resisting the urge to couple components together.
2. Composability for Integration
A backtesting library that forces you into its own runtime, data model, or lifecycle becomes a walled garden. You end up writing adapters to get data in, adapters to get results out, and workarounds whenever your workflow doesn't match the framework's assumptions.
tzutrader avoids this by keeping its boundaries thin. Data enters as a stream — in the simplest case, plain CSV on stdin. Results exit as a flat key-value string on stdout. This means the library slots naturally into Unix pipelines: fetch data with a shell script, run the backtest, pipe results into awk or column for formatting, feed them into a Python analysis notebook. No special export formats, no proprietary connectors, no mandatory GUI.
The internal design reinforces this. The Runner doesn't own the event loop — you call it. The Portfolio doesn't prescribe how results are consumed. Strategies and indicators are composed at the call site, not registered into a framework. Each component does its job and hands off cleanly, which means any of them can be replaced or wrapped without disturbing the rest.
3. Streaming Over Vectorization
Financial data arrives sequentially in real trading. Backtests should too. Processing data point-by-point:
- Prevents lookahead bias (can't peek at future data)
- Uses constant memory regardless of dataset size
- Mimics how live trading systems actually work
- Forces you to think about state management
Why this matters:
Vectorized libraries (pandas, numpy) are fast and convenient for analysis. But they make it easy to accidentally use future data:
# Easy to accidentally introduce lookahead bias
df['signal'] = (df['close'].shift(-1) > df['close']) # Using tomorrow's price!
With streaming, you physically can't access data that hasn't arrived yet:
// Only current data is available
Signal update(const Ohlcv& data) {
double ma = sma.update(data.close); // Can only use current and past data
// No way to peek ahead
}
This constraint is a feature, not a limitation.
3. Simplicity and Focus
Small is beautiful. The library does one thing: backtest strategies on historical data. It doesn't:
- Fetch data from APIs
- Provide a GUI
- Connect to live brokers
This narrow focus means:
- Less code to understand and maintain
- Fewer dependencies to manage
- Clearer mental model
- Room to experiment without breaking things
You can add these features yourself if needed. The library provides building blocks, not a complete platform.
4. No Magic
All behavior should be understandable by reading the code. No hidden state, no implicit behavior, no "framework magic."
What you see is what you get:
class SMA {
std::vector<double> buffer; // Visible state
size_t pos;
double sum;
double update(double value) {
// Clear logic, no hidden behavior
if (len < window) {
len++;
} else {
sum -= buffer[pos];
}
sum += value;
buffer[pos] = value;
pos = (pos + 1) % window;
return sum / window;
}
};
You can trace exactly what happens on each update. No surprises.
5. Performance Through Simplicity
Fast code doesn't need to be complex. Simple algorithms with good data structures often outperform "clever" code.
Design choices for performance:
- Circular buffers for rolling windows (O(1) updates)
- Templates for zero-cost abstraction
- Minimal dynamic allocation in hot paths
- Streaming to avoid loading entire datasets
But performance isn't the primary goal—clarity is. Fast code that's unmaintainable isn't useful.
Unix Philosophy Influence
Write programs that do one thing and do it well. Write programs to work together. — Doug McIlroy
The Unix philosophy shapes tzutrader's design:
Small, Focused Tools
Each component does one thing:
- Indicators calculate values
- Strategies generate signals
- Portfolios manage positions
- Runners orchestrate the process
Like Unix commands (grep, sort, cut), you combine them to create complex behavior.
Text Streams as Universal Interface
Unix programs communicate through text streams. tzutrader uses standard interfaces:
# Unix pipeline
cat data.csv | grep "2024" | cut -d',' -f5 | awk '{sum+=$1} END {print sum}'
# tzutrader pipeline
cat data.csv | ./my_backtest | tr ' ' '\n' | column -t -s ':'
CSV in, metrics out. Simple, composable, scriptable.
Sharp Tools
Each component should be a "sharp tool"—simple interface, powerful when composed:
// Sharp tools
SMA sma(20);
EMA ema(20);
RSI rsi(14);
// Composed into something more powerful
class MyStrategy {
SMA sma;
RSI rsi;
// Use both together
};
Mechanism, Not Policy
The library provides mechanisms (indicators, signal generation, position tracking) but doesn't enforce policies (which strategy to use, how to size positions).
You decide the policies. The library just provides the tools.
All of this is is what the Unix philosophy actually means in practice: not just that components are modular internally, but that the library itself is a good citizen in a larger system it doesn't control.
Who This Is For
Target Users
Primary audience:
- Intermediate to advanced programmers comfortable with C++
- Traders who want systematic strategy testing
- Students learning about algorithmic trading
- Developers interested in financial software architecture
This is NOT for:
- Complete programming beginners (C++ is hard)
- People wanting plug-and-play solutions (requires coding)
- Users wanting extensive hand-holding (minimal docs, you read code)
Why C++?
C++ is harder than Python. So why use it?
Learning value:
- Forces you to think about memory and state
- No hiding behind framework magic
- Closer to how production systems work
- Understanding performance implications
Performance:
- 10-100x faster than Python for compute-heavy tasks
- Matters when testing thousands of parameter combinations
- Low latency when processing large datasets
Production relevance:
- Most serious trading systems use C++ (or C, Java, Rust)
- Skills transfer to professional environment
- Understanding systems-level concerns
Trade-offs:
- Steeper learning curve than Python
- More verbose or less readable code
- Harder to prototype quickly
- Less forgiving of mistakes
If you're learning backtesting, Python is easier. If you're learning systems programming or want production-relevant skills, C++ is valuable.
Why C++ and not Rust?
The honest answer is that this is a learning project targeting skills that transfer to professional trading systems — and those systems are written in C++. That context shapes everything. Rust is the better language for new safety-critical software in isolation. Its memory safety guarantees, superior build tooling, and modern type system are genuine advantages. If tzutrader were a standalone application, Rust would be the defensible default.
But tzutrader is designed as a composable library — a building block meant to integrate with real-world financial infrastructure. That changes the calculus. Bloomberg terminals, vendor SDKs, QuantLib, and most high-performance data providers expose C/C++ interfaces. Rust can consume these via FFI, but doing so correctly requires careful unsafe blocks and often non-trivial wrapper code. C++ composes with this ecosystem with no friction at all.
There's also a subtler point: the C++ template system, for all its notorious complexity, is exceptionally well-suited to zero-cost composability. The CRTP patterns used throughout tzutrader — where strategies, indicators, and runners are composed at compile time with no virtual dispatch overhead — are idiomatic C++. Rust's traits and generics can achieve similar results, but the patterns are different, and the existing literature on this style of systems design is written in C++.
The real costs of this choice are serious and shouldn't be minimized. C++ gives you memory bugs, subtle undefined behavior, and CMake. These aren't just inconveniences — a silent memory corruption in portfolio state is financially dangerous even in a backtesting context. This is the price of the choice, and it demands disciplined, conservative code.
The bottom line: C++ here is not a claim that it's a better language in general. It's the right tool for someone learning the architecture of professional trading systems, working in the ecosystem those systems inhabit.
Prerequisites
To use tzutrader effectively, you should understand:
C++ fundamentals:
- Classes and templates
- Standard library containers
- Memory management basics
- Build systems (CMake)
Trading basics:
- What indicators measure
- How signals translate to trades
- Risk management concepts
- Why backtesting is hard
Unix/command-line:
- Basic shell commands
- Pipes and redirection
- Text processing tools
If you're missing any of these, tzutrader will be frustrating. That's okay—it's not for everyone.
What This Means in Practice
You Will Write Code
This isn't a GUI application where you click buttons. You write C++ code to define strategies:
class MyStrategy: public tzu::Strategy<MyStrategy, tzu::Ohlcv> {
tzu::SMA fast;
tzu::SMA slow;
public:
MyStrategy() : fast(10), slow(30) {}
tzu::Signal update(const tzu::Ohlcv& data) {
// Your logic here
}
};
If you're not comfortable writing and compiling C++, this isn't the right tool.
You Will Read Code
Documentation is minimal by design. To understand how things work, read the headers in include/tzu/. They're commented and relatively short.
If you expect comprehensive documentation explaining every detail, you'll be disappointed. The code IS the documentation.
You Will Experiment
This is experimental software. You'll encounter rough edges. You might need to fix bugs yourself. You'll definitely need to extend it for your needs.
If you want stable, well-supported software, use a mature library. This is for people who want to understand and modify their tools.
You Will Think About Design
The architecture is intentionally visible. You see how components connect, where state lives, how data flows. This is educational but requires engagement.
If you just want to run backtests without understanding the internals, other tools are better suited.
Non-Goals
Just as important as what tzutrader is—here's what it's not trying to be:
Not a complete trading platform: No data feeds, no broker connections, no live trading support.
Not beginner-friendly: Assumes minimal C++ proficiency and trading knowledge.
Not feature-complete: Minimal built-in strategies and indicators. You build what you need.
Not production-ready: Experimental, APIs can change, bugs exist.
Not optimized to death: Performance is good, not obsessive. Clarity over speed.
Not a framework: Provides libraries, not a framework you inherit from.
Why This Matters
These design principles aren't arbitrary. They serve specific goals:
For learning: Small, understandable components teach better than massive frameworks.
For experimentation: Easy to modify and extend when the architecture is simple.
For correctness: Streaming prevents lookahead bias; composability enables testing in isolation.
For longevity: Simple code with few dependencies ages better than complex systems.
This is an opinionated approach. It won't suit everyone. That's fine—use what works for you.
Evolution
Design philosophy guides decisions but isn't dogma. As I learn what works, things will change:
- Components might be refactored
- Interfaces might be simplified
- New patterns might emerge
- Bad ideas will be abandoned
The principles provide direction, not constraints. If violating a principle makes the library genuinely better, I'll violate it.
This is an experiment. Experiments mean learning, and learning means changing.
Contributing to the Philosophy
If you use tzutrader, your feedback shapes its evolution:
- What works well?
- What's confusing?
- Where do the principles help?
- Where do they hurt?
File issues, start discussions, share your experience. Philosophy emerges from practice, not theory.
The goal is finding patterns that make backtesting code clear, composable, and correct. If you discover better patterns, share them.