A C# algorithmic trading library for post-market stock strategy evaluation. Supports backtesting with daily/weekly/monthly timeframes, cash accounts, and long positions.
$ dotnet add package Codezerg.AlgoTraderA C# algorithmic trading library for post-market stock strategy evaluation and backtesting.
AlgoTrader is intentionally designed with specific constraints to focus on strategy evaluation:
| Constraint | Description |
|---|---|
| Cash accounts only | No margin trading |
| Long positions only | No short selling |
| Daily timeframe and above | D1, W1, MN1 only |
| Bar data only | No tick data support |
| Complete fills only | No partial order fills |
| Post-market execution | Orders execute at bar close |
dotnet add package Codezerg.AlgoTrader
Or via the Package Manager:
Install-Package Codezerg.AlgoTrader
dotnet build
dotnet test
using AlgoTrader.Indicators;
using AlgoTrader.Strategy;
public class GoldenCrossStrategy : StrategyBase
{
private iSMA? _fastMa;
private iSMA? _slowMa;
// Anchor symbol - determines when OnEvaluate() fires
public override string Symbol => "SPY";
protected override void OnInit()
{
// Initialize indicators
_fastMa = new iSMA(Close, 50);
_slowMa = new iSMA(Close, 200);
}
protected override void OnEvaluate()
{
// Update indicators
_fastMa!.Update();
_slowMa!.Update();
if (!_fastMa.Ready || !_slowMa.Ready)
return;
// Golden cross: fast MA crosses above slow MA
var goldenCross = _fastMa[1] <= _slowMa[1] && _fastMa[0] > _slowMa[0];
// Death cross: fast MA crosses below slow MA
var deathCross = _fastMa[1] >= _slowMa[1] && _fastMa[0] < _slowMa[0];
if (goldenCross && !HasPosition(Symbol))
{
var shares = SharesForAmount(Symbol, EquityPercent(95m));
if (shares > 0)
Buy(Symbol, shares);
}
else if (deathCross && HasPosition(Symbol))
{
ClosePosition(Symbol);
}
}
}
using AlgoTrader.Broker;
using AlgoTrader.Core;
using AlgoTrader.Data;
// Create data provider (CSV or EODHD)
var dataProvider = new CsvDataProvider("./data");
// Or: var dataProvider = new EodhdDataProvider(apiKey);
// Create broker with initial capital
var broker = new BacktestBroker(100_000m, commissionPerShare: 0.01m);
// Configure engine
var config = new EngineConfig
{
Timeframe = Timeframe.D1,
WarmupBars = 200 // Allow indicators to initialize
};
var engine = new Engine(dataProvider, broker, config);
engine.AddStrategy(new GoldenCrossStrategy());
// Run backtest
var result = await engine.RunAsync("2020-01-01", "2024-12-31");
// Print results
Console.WriteLine(result.Summary());
═══════════════════════════════════════════════════════════════
BACKTEST RESULTS
═══════════════════════════════════════════════════════════════
Period: 2020-01-01 to 2024-12-31
Initial Equity: $100,000.00
Final Equity: $142,350.00
───────────────────────────────────────────────────────────────
RETURNS
───────────────────────────────────────────────────────────────
Total Return: 42.35% ($42,350.00)
Annualized: 8.47%
───────────────────────────────────────────────────────────────
RISK
───────────────────────────────────────────────────────────────
Sharpe Ratio: 0.85
Max Drawdown: 15.23% ($15,230.00)
───────────────────────────────────────────────────────────────
TRADES
───────────────────────────────────────────────────────────────
Total Trades: 12
Win Rate: 58.33% (7W / 5L)
Profit Factor: 2.15
Strategy Layer (user strategies)
↓
Engine Layer (orchestrates execution)
↓
┌───┴───┐
↓ ↓
Data Broker
src/AlgoTrader/
├── Core/ # Engine, types, events, configuration
├── Domain/ # Bar, Order, Position, Account
├── Data/ # DataSeries, SymbolData, providers
├── Broker/ # IBroker, BacktestBroker, Trade
├── Strategy/ # StrategyBase, StrategyContext
├── Indicators/ # SMA, EMA, RSI, MACD, Bands, ATR
└── Analysis/ # Metrics, BacktestResult
examples/ # Example strategies
tests/ # Unit tests
public class MyStrategy : StrategyBase
{
public override string Symbol => "SPY"; // Anchor symbol
protected override void OnInit()
{
// Called once at start - initialize indicators
}
protected override void OnEvaluate()
{
// Called after each bar - implement trading logic
}
protected override void OnDeinit()
{
// Called once at end - cleanup
}
}
Data is accessed using reverse indexing where 0 is the most recent bar:
// Anchor symbol data (shorthand)
Close[0] // Current bar close
Close[1] // Previous bar close
Open[0] // Current bar open
High[0] // Current bar high
Low[0] // Current bar low
Volume(0) // Current bar volume
// Multi-symbol data access
Data["AAPL"].Close[0] // AAPL current close
Data["MSFT"].High[1] // MSFT previous high
// Submit orders
Buy(symbol, quantity); // Market buy
Buy(symbol, quantity, price, OrderType.Limit); // Limit buy
Sell(symbol, quantity); // Market sell
ClosePosition(symbol); // Close entire position
// Position info
HasPosition(symbol) // Check if has position OR pending buy
PositionSize(symbol) // Current shares held
// Sizing helpers
EquityPercent(50m) // 50% of current equity
SharesForAmount(symbol, amount) // Shares purchasable for amount
Orders placed in OnEvaluate() are queued and execute after the method returns:
OnEvaluate() called
↓
Buy/Sell orders queued
↓
OnEvaluate() returns
↓
Orders execute at bar close price
↓
Next bar begins
Use HasPosition(symbol) to check both existing positions AND pending buy orders, preventing duplicate orders in the same evaluation cycle.
All indicators use the i prefix naming convention:
| Indicator | Class | Description |
|---|---|---|
| SMA | iSMA | Simple Moving Average |
| EMA | iEMA | Exponential Moving Average |
| RSI | iRSI | Relative Strength Index (Wilder's smoothing) |
| MACD | iMACD | Moving Average Convergence Divergence |
| Bollinger Bands | iBands | Bollinger Bands (SMA +/- std dev) |
| ATR | iATR | Average True Range |
protected override void OnInit()
{
// Moving averages
var sma = new iSMA(Close, 20);
var ema = new iEMA(Close, 12);
// RSI
var rsi = new iRSI(Close, 14);
// MACD
var macd = new iMACD(Close, 12, 26, 9);
// Bollinger Bands
var bands = new iBands(Close, 20, 2.0m);
// ATR (requires SymbolData)
var atr = new iATR(Data["SPY"], 14);
}
protected override void OnEvaluate()
{
// Update indicators
sma.Update();
ema.Update();
rsi.Update();
macd.Update();
bands.Update();
atr.Update();
// Access values
if (sma.Ready)
{
var currentSma = sma[0];
var previousSma = sma[1];
}
// MACD lines
if (macd.Ready)
{
var macdLine = macd.Main[0];
var signalLine = macd.Signal[0];
var histogram = macd.Histogram[0];
}
// Bollinger Bands
if (bands.Ready)
{
var upper = bands.Upper[0];
var middle = bands.Middle[0];
var lower = bands.Lower[0];
}
}
Reads from local CSV files with naming convention {SYMBOL}_{TIMEFRAME}.csv:
data/
├── SPY_D1.csv
├── AAPL_D1.csv
└── MSFT_D1.csv
CSV Format:
date,open,high,low,close,volume
2020-01-02,323.54,324.89,322.53,324.87,32145000
2020-01-03,321.16,323.64,321.05,322.41,36156000
Usage:
var provider = new CsvDataProvider("./data");
Fetches data from the EODHD API:
// Set environment variable
// Windows: set EODHD_API_KEY=your_api_key
// Linux/Mac: export EODHD_API_KEY=your_api_key
var apiKey = Environment.GetEnvironmentVariable("EODHD_API_KEY");
await using var provider = new EodhdDataProvider(apiKey);
Account state (balance, positions, orders, trades) can be saved and loaded for two main use cases:
using Codezerg.AlgoTrader.Domain;
// Load existing account or create new one
Account account;
if (File.Exists("account.json"))
account = Account.LoadFromFile("account.json");
else
account = new Account(100_000m);
// Create broker with the account
var broker = new BacktestBroker(account, commissionPerShare: 0.01m);
// ... run strategy ...
// Save account state
account.SaveToFile("account.json");
Saved State Format (JSON):
{
"Balance": 95234.50,
"Positions": [
{
"Symbol": "SPY",
"Quantity": 100,
"AvgPrice": 450.25,
"OpenedAt": "2024-01-15T00:00:00Z",
"MarketPrice": 455.80
}
],
"Orders": [],
"Trades": [
{
"OrderId": "abc-123",
"Symbol": "SPY",
"Side": "Buy",
"Quantity": 100,
"Price": 450.25,
"Timestamp": "2024-01-15T00:00:00Z",
"Commission": 1.00,
"Pnl": 0
}
]
}
The examples/ directory contains ready-to-use strategies:
| Strategy | Description |
|---|---|
BuyAndHoldStrategy | Simple benchmark - buy once and hold |
GoldenCrossStrategy | 50/200 SMA crossover |
MeanReversionStrategy | RSI oversold/overbought with trend filter |
MomentumStrategy | Multi-symbol momentum rotation |
# Set API key (required for EODHD data)
export EODHD_API_KEY=your_api_key
# Run default example
dotnet run --project examples/Codezerg.AlgoTrader.Examples.csproj
# Run stateful example (demonstrates account persistence)
dotnet run --project examples/Codezerg.AlgoTrader.Examples.csproj -- --stateful
The backtest result includes comprehensive metrics:
| Metric | Description |
|---|---|
| Total Return | Percentage and dollar return |
| Annualized Return | Compound annual growth rate |
| Sharpe Ratio | Risk-adjusted return (assumes 2% risk-free rate) |
| Max Drawdown | Largest peak-to-trough decline |
| Total Trades | Number of completed round trips |
| Win Rate | Percentage of profitable trades |
| Profit Factor | Gross profit / gross loss |
| Average Win/Loss | Mean winning/losing trade |
| Largest Win/Loss | Best/worst single trade |
| Total Commission | Sum of all commissions paid |
decimal (not float/double) for all monetary valuesDateTime for all time-related valuesrecord typevar config = new EngineConfig
{
Timeframe = Timeframe.D1, // D1, W1, or MN1
WarmupBars = 200 // Bars before trading starts
};
MIT License - see LICENSE file for details.
dotnet test