đ Zack: A Simple Backtesting Engine in Zig đ
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.
đ¤ What is it?
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.
⨠Why Zig?
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!).
âī¸ How it Works: The Engine 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
mainfunction 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.parseto convert CSV rows into structuredBarobjects. - Portfolio Update (
Portfolio): The portfolio calculates its current market value based on thecurrent_bar.closeprice and any openPosition. It records the total equity at this point in time (EquityPoint). - Lookahead: The engine fetches the
next_barfrom theDataHandler. This is crucial for simulating execution delays. - Strategy Signal (
BuyAndHoldStrategy): The current strategy (BuyAndHoldStrategyin this case) receives thecurrent_bardata 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 aSignalis received, thePortfoliodetermines 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): TheOrderis sent to theExecutionHandler. Crucially, it uses thenext_bar.openprice 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 resultingFillevent 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_barbecoming thecurrent_bar.
- Data Handling (
-
Results: After processing all bars, the
BacktestEngine.logResultsfunction prints a summary of the performance.
đ¯ Current Strategy: Buy and Hold
The engine currently implements a simple "Buy and Hold" strategy (src/engine/strategy.zig).
- Logic: It generates a single "Buy" (
Long) signal when theopenprice 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
buyAtthreshold is set in the strategy's configuration file (e.g.,config/buy-and-hold.json):
đ ī¸ Configuration
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/
}đ Data Format
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.
đ Project Structure
.
âââ 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
đ How to Run
-
Ensure you have Zig installed (see ziglang.org).
-
Clone the repository.
-
Run the simulation using the Zig build system:
Alternatively, run the main file directly:
đ Example Output
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
Longsignal is generated based on the first bar (open=42000 >=buyAt=1000). - The
MarketBuyorder is executed at theopenprice 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.
đŽ Future Work
- 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!