Algorithmic Trading Basics: From Concept to Production
Introduction
Algorithmic trading -- the use of computer programs to execute trading strategies according to predefined rules -- accounts for approximately 60-75% of equity trading volume in U.S. markets. For quantitative practitioners, understanding the full stack from signal generation to order execution is essential. This article covers the foundational components: strategy taxonomy, system architecture, backtesting methodology, and the critical gap between simulation and live trading.
Strategy Taxonomy
Algorithmic trading strategies fall into several broad categories, each with distinct statistical properties and infrastructure requirements.
Trend Following
Trend-following strategies exploit serial correlation in asset returns. The simplest formulation uses moving average crossovers:
$$
Signal_t = \begin{cases} +1 & \text{if } MA_{fast,t} > MA_{slow,t} \\ -1 & \text{if } MA_{fast,t} < MA_{slow,t} \end{cases}
$$
A standard parameterization uses 50-day and 200-day simple moving averages. On the S&P 500 (1950-2025), the long-only version of this strategy produced an annual return of 8.1% with a maximum drawdown of -24%, versus buy-and-hold returns of 10.4% with a -56% drawdown. The strategy sacrifices some return for substantially better risk-adjusted performance.
Mean Reversion
Mean-reversion strategies bet that prices revert to a statistical equilibrium. The z-score framework is standard:
$$
z_t = \frac{P_t - \mu_t}{\sigma_t}
$$
where $\mu_t$ and $\sigma_t$ are the rolling mean and standard deviation over a lookback window. Entry signals trigger when $|z_t| > 2$ and exit when $|z_t| < 0.5$.
Statistical Arbitrage
Stat arb strategies exploit relative mispricings between related securities. Pairs trading is the canonical example:
$$
Spread_t = P_{A,t} - \beta \cdot P_{B,t} - \alpha
$$
where $\beta$ is the cointegration coefficient estimated via the Engle-Granger procedure or Johansen test.
Market Making
Market-making algorithms provide liquidity by continuously quoting bid and ask prices. The Avellaneda-Stoikov model provides the theoretical foundation:
$$
\delta^{bid/ask} = \frac{\gamma \sigma^2 (T-t)}{2} + \frac{1}{\gamma} \ln\left(1 + \frac{\gamma}{\kappa}\right)
$$
where $\gamma$ is the risk aversion parameter, $\sigma$ is volatility, and $\kappa$ controls order arrival intensity.
System Architecture
A production algorithmic trading system consists of several interconnected components:
[Market Data Feed] --> [Data Handler] --> [Signal Generator]
|
v
[Risk Manager] <-- [Portfolio Constructor] <-- [Alpha Model]
|
v
[Order Management System] --> [Execution Engine] --> [Exchange/Broker]
|
v
[Trade Database] --> [Performance Monitor] --> [Alerting System]
Core Implementation
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
import pandas as pd
import numpy as np
from typing import Dict, List, Optional
class Side(Enum):
BUY = 1
SELL = -1
@dataclass
class Signal:
timestamp: pd.Timestamp
symbol: str
side: Side
strength: float # [-1.0, 1.0]
metadata: dict = None
@dataclass
class Order:
timestamp: pd.Timestamp
symbol: str
side: Side
quantity: int
order_type: str # 'MARKET', 'LIMIT', 'TWAP'
limit_price: Optional[float] = None
class Strategy(ABC):
"""Base class for all trading strategies."""
@abstractmethod
def generate_signals(self, data: pd.DataFrame) -> List[Signal]:
"""Produce trading signals from market data."""
pass
@abstractmethod
def size_position(self, signal: Signal, portfolio: dict) -> int:
"""Convert signal strength to position size."""
pass
class MomentumStrategy(Strategy):
def __init__(self, fast_period: int = 20, slow_period: int = 60,
volatility_lookback: int = 20, risk_target: float = 0.10):
self.fast_period = fast_period
self.slow_period = slow_period
self.vol_lookback = volatility_lookback
self.risk_target = risk_target
def generate_signals(self, data: pd.DataFrame) -> List[Signal]:
signals = []
for symbol in data.columns.get_level_values(0).unique():
prices = data[symbol]['close']
fast_ma = prices.rolling(self.fast_period).mean()
slow_ma = prices.rolling(self.slow_period).mean()
# Normalize signal strength by distance between MAs
strength = (fast_ma.iloc[-1] - slow_ma.iloc[-1]) / prices.iloc[-1]
strength = np.clip(strength * 10, -1, 1) # Scale to [-1, 1]
if abs(strength) > 0.1: # Minimum threshold
signals.append(Signal(
timestamp=prices.index[-1],
symbol=symbol,
side=Side.BUY if strength > 0 else Side.SELL,
strength=strength
))
return signals
def size_position(self, signal: Signal, portfolio: dict) -> int:
"""Volatility-targeted position sizing."""
nav = portfolio['nav']
price = portfolio['prices'][signal.symbol]
vol = portfolio['volatilities'][signal.symbol]
# Target dollar risk per position
dollar_risk = nav self.risk_target abs(signal.strength)
# Shares needed to achieve target risk
dollar_vol_per_share = price * vol / np.sqrt(252)
shares = int(dollar_risk / dollar_vol_per_share)
return shares * signal.side.value
Backtesting: The Scientific Method of Trading
Backtesting is hypothesis testing applied to trading strategies. The goal is to estimate how a strategy would have performed historically while avoiding statistical biases.
Event-Driven Backtester
class Backtester:
def __init__(self, strategy: Strategy, data: pd.DataFrame,
initial_capital: float = 1_000_000,
commission_per_share: float = 0.005,
slippage_pct: float = 0.001):
self.strategy = strategy
self.data = data
self.capital = initial_capital
self.commission = commission_per_share
self.slippage = slippage_pct
self.positions: Dict[str, int] = {}
self.equity_curve = []
def run(self) -> pd.DataFrame:
"""Execute backtest and return performance metrics."""
for t in range(max(60, self.strategy.slow_period), len(self.data)):
window = self.data.iloc[:t+1]
signals = self.strategy.generate_signals(window)
# Mark to market
portfolio_value = self.capital
for sym, qty in self.positions.items():
portfolio_value += qty * self.data[sym]['close'].iloc[t]
self.equity_curve.append({
'timestamp': self.data.index[t],
'portfolio_value': portfolio_value
})
# Execute signals
for signal in signals:
portfolio = {
'nav': portfolio_value,
'prices': {signal.symbol: self.data[signal.symbol]['close'].iloc[t]},
'volatilities': {signal.symbol: self.data[signal.symbol]['close'].pct_change().rolling(20).std().iloc[t]}
}
target_qty = self.strategy.size_position(signal, portfolio)
current_qty = self.positions.get(signal.symbol, 0)
trade_qty = target_qty - current_qty
if trade_qty != 0:
self._execute_trade(signal.symbol, trade_qty, t)
return pd.DataFrame(self.equity_curve).set_index('timestamp')
def _execute_trade(self, symbol: str, quantity: int, t: int):
price = self.data[symbol]['close'].iloc[t]
slippage_cost = abs(quantity) price self.slippage
commission_cost = abs(quantity) * self.commission
self.capital -= quantity * price + slippage_cost + commission_cost
self.positions[symbol] = self.positions.get(symbol, 0) + quantity
Critical Biases to Avoid
| Bias | Description | Mitigation | |------|-------------|------------| | Look-ahead | Using data not yet available at decision time | Strict time indexing; shift signals by 1 bar | | Survivorship | Only testing on securities that still exist | Use point-in-time constituent lists | | Overfitting | Strategy tuned to historical noise | Walk-forward optimization; out-of-sample validation | | Transaction cost | Ignoring real-world execution costs | Model spread, slippage, market impact | | Selection | Only reporting the best of many tested strategies | Adjust for multiple comparisons (Bonferroni, FDR) |The probability of finding a spuriously significant strategy after N trials is:
$$
P(\text{false discovery}) = 1 - (1 - \alpha)^N
$$
With $\alpha = 0.05$ and $N = 100$ strategy variants tested, the probability of at least one false discovery is 99.4%. This is why multiple comparison adjustment is essential.
Performance Metrics
Every algorithmic strategy should be evaluated on these core metrics:
def compute_metrics(equity_curve: pd.Series) -> dict:
returns = equity_curve.pct_change().dropna()
annual_return = returns.mean() * 252
annual_vol = returns.std() * np.sqrt(252)
sharpe = annual_return / annual_vol
# Maximum drawdown
cumulative = (1 + returns).cumprod()
running_max = cumulative.cummax()
drawdown = (cumulative - running_max) / running_max
max_drawdown = drawdown.min()
# Calmar ratio
calmar = annual_return / abs(max_drawdown) if max_drawdown != 0 else np.inf
# Sortino ratio
downside_returns = returns[returns < 0]
downside_vol = downside_returns.std() * np.sqrt(252)
sortino = annual_return / downside_vol if downside_vol > 0 else np.inf
return {
'annual_return': f"{annual_return:.2%}",
'annual_volatility': f"{annual_vol:.2%}",
'sharpe_ratio': round(sharpe, 2),
'sortino_ratio': round(sortino, 2),
'max_drawdown': f"{max_drawdown:.2%}",
'calmar_ratio': round(calmar, 2),
'win_rate': f"{(returns > 0).mean():.2%}",
'profit_factor': round(returns[returns > 0].sum() / abs(returns[returns < 0].sum()), 2)
}
From Backtest to Production
The gap between a profitable backtest and a profitable live strategy is where most algorithmic traders fail. Key considerations:
Latency: Backtesters execute instantly; real markets do not. A strategy that depends on filling at the close price may slip by 5-20 basis points in practice. Always add realistic latency to your simulation. Capacity: A strategy that generates $1M in backtest profits may only support $10M in live capital before market impact erodes returns. Estimate capacity as the strategy's dollar volume divided by 1-5% of the average daily volume of traded instruments. Infrastructure: Production systems require redundancy, monitoring, and automated failover. Use message queues (Redis, RabbitMQ) between components, implement circuit breakers, and always have a manual kill switch.Conclusion
Algorithmic trading combines quantitative analysis, software engineering, and risk management into a unified discipline. The fundamentals remain constant: generate signals with statistical edge, size positions to manage risk, execute efficiently to minimize slippage, and rigorously backtest while accounting for the many biases that plague historical simulation. Mastery of these basics provides the foundation for more advanced strategies including machine learning, high-frequency trading, and cross-asset arbitrage.
Frequently Asked Questions
What programming language should I use for algorithmic trading?
Python dominates research and prototyping due to its ecosystem (pandas, numpy, scikit-learn). For production execution, C++ or Rust provide the low-latency performance needed for high-frequency strategies. Many firms use Python for signal generation and C++ for order execution. Java and C# are common in institutional settings with existing infrastructure.
How much capital do I need to start algorithmic trading?
For equities, $25,000 minimum (SEC pattern day trading rule in the U.S.) but $100,000+ is practical for meaningful diversification. Futures require less capital due to leverage -- $50,000 can trade a diversified futures portfolio. Crypto markets have no minimums. The key constraint is that position sizes must be large enough that transaction costs do not consume your edge.
What is a good Sharpe ratio for a trading strategy?
A Sharpe ratio above 1.0 is considered good for a daily-frequency strategy. Above 2.0 is excellent and typically found only in high-frequency or capacity-constrained strategies. Be skeptical of backtested Sharpes above 3.0 -- they usually indicate overfitting. In live trading, expect Sharpe ratios to degrade by 30-50% from backtest estimates.
How do I know if my strategy is overfitted?
Compare in-sample and out-of-sample performance. A strategy with a backtest Sharpe of 2.5 that drops to 0.3 out of sample is overfitted. Use the deflated Sharpe ratio to account for the number of trials. Keep strategies simple (fewer parameters) and ensure the economic logic is sound. If you cannot explain why the strategy works, it probably does not.
What is the difference between paper trading and backtesting?
Backtesting replays historical data and simulates execution. Paper trading runs the strategy in real-time against live market data but with simulated orders. Paper trading catches bugs that backtesting misses: data feed issues, order rejection handling, timezone problems, and latency effects. Always paper trade for at least 2-4 weeks before deploying real capital.