Welcome to Zack! This project is a lightweight yet powerful backtesting engine for trading strategies, written entirely in Zig ⚡. It allows you to test your trading ideas against historical market data to see how they might have performed.
Zack simulates the process of trading based on a predefined strategy using historical OHLCV (Open, High, Low, Close, Volume) data. It processes data bar-by-bar, generates trading signals, simulates order execution, manages a virtual portfolio, and reports the performance.
Zig offers several advantages for this kind of application:
- Performance: Zig compiles to fast, efficient machine code, crucial for processing potentially large datasets quickly.
- Memory Control: Manual memory management allows for fine-tuned optimization and avoids hidden overhead.
- Simplicity: Zig's focus on simplicity and explicitness makes the codebase easier to understand and maintain (no hidden control flow!).
The backtesting process is driven by an event loop within the BacktestEngine
. Here's a breakdown of the core components and their interactions:
-
Initialization:
- The
main
function loads configuration (config/config.json
,config/<strategy_name>.json
) and CSV data (data/<data_file>.csv
) usingAppContext
. - It then initializes the
BacktestEngine
, which in turn sets up all other components.
- The
-
The Event Loop (
BacktestEngine.run
): The engine iterates through the historical data bar by bar. For eachcurrent_bar
:- Data Handling (
DataHandler
): Provides thecurrent_bar
(parsed from the CSV data). It usesBar.parse
to convert CSV rows into structuredBar
objects. - Portfolio Update (
Portfolio
): The portfolio calculates its current market value based on thecurrent_bar.close
price and any openPosition
. It records the total equity at this point in time (EquityPoint
). - Lookahead: The engine fetches the
next_bar
from theDataHandler
. This is crucial for simulating execution delays. - Strategy Signal (
BuyAndHoldStrategy
): The current strategy (BuyAndHoldStrategy
in this case) receives thecurrent_bar
data and the portfolio's state (e.g.,has_position
). It decides if a trading signal (Signal
) should be generated based on its rules (e.g.,bar.open >= buyAt
).// Inside strategy.generateSignal if (!has_position and bar.open >= @as(f64, @floatFromInt(self.config.buyAt))) { return Signal{ .type = .Long }; // Generate Buy signal }
- Order Generation (
Portfolio
): If aSignal
is received, thePortfolio
determines the details of theOrder
(e.g.,MarketBuy
, quantity). It might use thecurrent_bar
's price for approximate sizing.// Inside portfolio.handleSignal const quantity_to_buy = cash_to_use / current_bar.close; return Order{ .type = .MarketBuy, .quantity = quantity_to_buy };
- Execution Simulation (
ExecutionHandler
): TheOrder
is sent to theExecutionHandler
. Crucially, it uses thenext_bar.open
price to simulate the fill, modeling the delay between deciding to trade and the order actually executing in the next period. It also calculates commission.// Inside execution_handler.executeOrder const fill_price = next_bar.open; // Fill at NEXT bar's open const commission = COMMISSION_PER_TRADE; return Fill{ /* ...details... */ };
- Portfolio Update (
Portfolio
): The resultingFill
event is sent back to thePortfolio
, which updates itscurrent_cash
,position
, andcurrent_holdings_value
.// Inside portfolio.handleFill self.current_cash -= cost; self.position = Position{ .entry_price = fill.fill_price, /*...*/ };
- Loop: The process repeats with the
next_bar
becoming thecurrent_bar
.
- Data Handling (
-
Results: After processing all bars, the
BacktestEngine.logResults
function prints a summary of the performance.
The engine currently implements a simple "Buy and Hold" strategy (src/engine/strategy.zig
).
- Logic: It generates a single "Buy" (
Long
) signal when theopen
price of a bar crosses above a predefined threshold (buyAt
), but only if the portfolio does not already hold a position. It never generates a sell signal; the position is held until the end of the backtest. - Configuration: The
buyAt
threshold is set in the strategy's configuration file (e.g.,config/buy-and-hold.json
):{ "buyAt": 1000 }
The main simulation parameters are set in config/config.json
:
{
"budget": 10000, // Initial capital for the simulation
"strategy": "buy-and-hold.json", // Which strategy config file to load from config/
"data": "btc.csv" // Which data file to load from data/
}
The engine expects OHLCV data in CSV format in the data/
directory:
timestamp,open,high,low,close,volume
2024-01-01T00:00:00Z,42000.00,42100.00,41900.00,42050.00,100.50
2024-01-01T01:00:00Z,42050.00,42200.00,42000.00,42150.00,120.75
...
timestamp
: ISO 8601 format (currently treated as a string).open
,high
,low
,close
,volume
: Floating-point numbers.
.
├── build.zig # Zig build script
├── config/
│ ├── config.json # Main configuration
│ └── buy-and-hold.json # Strategy-specific parameters
├── data/
│ └── btc.csv # Sample OHLCV data
├── src/
│ ├── main.zig # Application entry point
│ ├── csv/ # CSV parser utility
│ │ └── csv-parser.zig
│ ├── engine/ # Core backtesting engine components
│ │ ├── common.zig # Shared structs (Bar, Signal, Order, Fill, Position)
│ │ ├── data_handler.zig # Loads and provides Bars
│ │ ├── strategy.zig # Strategy logic (BuyAndHoldStrategy)
│ │ ├── portfolio.zig # Manages cash, position, equity
│ │ ├── execution_handler.zig # Simulates order fills
│ │ └── backtest_engine.zig # Orchestrates the simulation loop
│ └── utils/ # Utility functions
│ ├── load-config.zig # JSON config loading
│ └── logger.zig # Simple logging utility
└── README.md # This file
-
Ensure you have Zig installed (see ziglang.org).
-
Clone the repository.
-
Run the simulation using the Zig build system:
zig build run
Alternatively, run the main file directly:
zig run src/main.zig
Running the engine with the default configuration and sample btc.csv
data produces output similar to this:
ℹ️ [INFO] ⚙️ Configuration Loaded:
ℹ️ [INFO] Budget: 10000
ℹ️ [INFO] Strategy: buy-and-hold.json
ℹ️ [INFO] Data File:btc.csv
ℹ️ [INFO] 📈 Strategy Settings:
ℹ️ [INFO] Buy At Threshold: 1000
--- Starting Backtest Run ---
PORTFOLIO: Received LONG signal, generating MarketBuy order for ~0.23547619047619048 units.
EXECUTION: Executing MarketBuy order for 0.23547619047619048 units @ 42050 (Commission: 1)
PORTFOLIO: Handled MarketBuy fill. Cash: 9.99999999999909, Position Qty: 0.23547619047619048, Entry: 42050
--- Backtest Run Finished ---
ℹ️ [INFO]
📊 Backtest Results:
ℹ️ [INFO] Initial Capital: 10000.00
ℹ️ [INFO] Final Equity: 10443.75
ℹ️ [INFO] Total Return: 4.44%
ℹ️ [INFO] Ending Position: 0.2355 units @ entry 42050.00
ℹ️ [INFO] (More detailed performance metrics TBD)
Application finished successfully.
(Note: Exact float values might differ slightly)
Key Observations from Output:
- The
Long
signal is generated based on the first bar (open
=42000 >=buyAt
=1000). - The
MarketBuy
order is executed at theopen
price of the second bar (42050), as expected due to the one-bar delay simulation. - The final equity reflects the initial capital minus the buy cost plus the value of the holding at the final bar's close price.
- Implement more sophisticated performance metrics (Sharpe Ratio, Max Drawdown, etc.).
- Implement more strategies.
- Implement technical indicators.
- Add comprehensive unit tests for all engine components.
Contributions and suggestions are welcome!