Three years ago, a systematic fund with $50M AUM had 12 strategies running in production. Today, with $200M and a team of eight quants, they have 23 strategies — and the ops team cannot tell you with confidence which three are currently bleeding money.
This is not a capacity problem. It is a process problem.
Small quantitative teams often treat strategy development as a craft: each quant writes code their own way, maintains their own backtest, and deploys when "it feels ready." At five strategies, this works. At twenty, it becomes a liability. A single malformed deployment corrupts performance attribution. An undocumented parameter change makes replication impossible. A backtest without standardized cost assumptions generates false confidence.
This article presents a production-grade strategy development pipeline designed for teams of two to fifteen quants. The framework covers the full lifecycle — from idea capture through live monitoring — with specific standards for backtest documentation, code quality, and deployment checklists. All code examples use TickDB for data acquisition and real-time monitoring.
The Core Problem: Strategy Entropy
Before defining the solution, it is worth cataloging the failure modes that make strategy entropy inevitable.
Documentation rot: Strategy rationale, parameter history, and assumptions live in a quant's head or in a Slack thread from eighteen months ago. When that quant leaves, institutional knowledge evaporates.
Backtest inconsistency: One quant assumes 5 bps commission; another assumes 2 bps. Slippage models vary. Venue assumptions differ. Comparing strategy performance becomes meaningless.
Deployment variance: "It worked on my machine" is not a deployment standard. Scripts that work in a Jupyter notebook may fail silently in a cron job. No heartbeat monitoring means silent failures go undetected for hours.
Risk boundary drift: A strategy designed for liquid large-caps drifts into small-caps because "the signals are strong there." Position limits are not enforced. Correlation with existing books is not checked.
The pipeline below addresses each failure mode with specific, enforceable standards.
The Strategy Lifecycle: Seven Phases
Every strategy in the team's portfolio must traverse seven defined phases. Skipping phases is not permitted. Accelerated timelines require written approval from the head of quant research.
| Phase | Name | Entry criteria | Exit criteria | Typical duration |
|---|---|---|---|---|
| 1 | Idea capture | Observation, hypothesis, or signal discovery | Written one-pager in the strategy registry | 1–3 days |
| 2 | Research | Back-of-envelope analysis, literature review | Hypothesis formalized; data requirements documented | 1–2 weeks |
| 3 | Backtesting | Clean historical dataset, standardized cost model | Backtest report with full statistics; no critical warnings | 2–4 weeks |
| 4 | Paper trading | Production-equivalent code, paper trading API | 20+ trading days; performance within expected bounds | 2–4 weeks |
| 5 | Pre-launch review | All review gates passed | Signed deployment approval | 3–5 days |
| 6 | Live deployment | Deployment checklist complete | Strategy in production; monitoring active | 1 day |
| 7 | Live monitoring | N/A | Quarterly performance review; annual reauthorization | Ongoing |
Phase 1: Idea Capture — The Strategy Registry
Every strategy begins as a single document in the team's strategy registry. The registry is a structured database (Notion, Linear, or a custom tool) where ideas are logged with:
- Hypothesis: A falsifiable statement. Not "momentum works" but "intraday reversal in NYSE-listed equities after a 3σ price spike resolves within the next 15 minutes with a 60% win rate."
- Initial signal definition: The exact metric or combination of metrics that triggers a position.
- Intended market scope: Asset class, exchange, market cap range, liquidity minimum.
- Estimated capacity: Expected AUM capacity before signal degradation.
- Primary risk: The single most likely failure mode.
The registry enforces a template. Quants cannot submit ideas without completing all fields. This prevents the "I had a great idea six months ago but I cannot remember what it was" failure mode.
Phase 3: Backtesting — Standardized Cost Model
Backtest standards are where most small teams fail. Without a standardized cost model, backtests become exercises in creative accounting.
3.1 Mandatory Cost Assumptions
The team operates from a single shared cost model. All backtests must use these parameters without modification unless a variance is explicitly documented and approved.
| Cost component | Assumption | Source |
|---|---|---|
| Commission | $0.005 per share | Tier-1 broker average |
| Market impact | 1.5× bid-ask spread | Square-root law, $500K average order size |
| Slippage | 0.5 bps | Execution quality study (annual) |
| Borrow rate | Current benchmark + 50 bps | External data feed |
| Margin rate | 1.5× short rate | Prime broker terms |
Any backtest that uses different assumptions must include a sensitivity table showing strategy returns under at least three cost scenarios: optimistic (0.75×), baseline, and pessimistic (1.5×).
3.2 Backtest Report Requirements
A backtest is not complete until a standardized report exists. The report must include:
- Period: Start date, end date, and justification for the period (must include at least one full market cycle or 3 years, whichever is shorter).
- Sample size: Number of trades. Minimum 500 trades for equity strategies; minimum 200 trades for options or futures.
- Performance metrics: Annualized return, volatility, Sharpe ratio, Sortino ratio, max drawdown, drawdown duration.
- Trade-level statistics: Win rate, average win, average loss, profit factor, largest winning trade, largest losing trade.
- Benchmark comparison: Buy-and-hold return over the same period, with attribution analysis.
- Regime breakdown: Performance in bull, bear, and sideways markets (minimum three regimes).
- Critical warnings: Any metric that falls below the team's minimum threshold triggers a critical warning. For example: Sharpe < 1.0, win rate < 45%, max drawdown > 20%.
3.3 Backtest Code Standards
Backtest code must be version-controlled, documented, and reproducible. The following Python template establishes the minimum standard for all backtest implementations.
"""
Strategy backtest template.
Team standard: all backtests must inherit from this structure.
Last updated: 2026-04
"""
import os
import hashlib
import json
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import Optional
# Cost model constants — team standard, do not modify without approval
COMMISSION_PER_SHARE = 0.005 # USD
MARKET_IMPACT_MULTIPLIER = 1.5 # × bid-ask spread
SLIPPAGE_BPS = 0.5 # basis points
BORROW_RATE_SPREAD = 0.50 # bps over benchmark
@dataclass
class BacktestConfig:
"""Standard configuration object for all strategy backtests."""
strategy_id: str
start_date: str # ISO format: YYYY-MM-DD
end_date: str # ISO format: YYYY-MM-DD
initial_capital: float = 1_000_000.0
commission: float = COMMISSION_PER_SHARE
slippage_bps: float = SLIPPAGE_BPS
market_impact_multiplier: float = MARKET_IMPACT_MULTIPLIER
# Risk parameters
max_position_pct: float = 0.05 # 5% max per position
max_drawdown_exit_pct: float = 0.15 # Exit if drawdown exceeds 15%
def config_hash(self) -> str:
"""Generate a unique hash for this configuration.
Used to detect accidental parameter drift between backtest runs.
Any change to config must be explicitly reviewed.
"""
config_str = json.dumps(self.__dict__, sort_keys=True)
return hashlib.sha256(config_str.encode()).hexdigest()[:12]
@dataclass
class BacktestResult:
"""Container for backtest output statistics."""
config: BacktestConfig
total_return: float
sharpe_ratio: float
sortino_ratio: float
max_drawdown: float
max_drawdown_duration_days: int
win_rate: float
profit_factor: float
num_trades: int
def to_report_dict(self) -> dict:
"""Export as dictionary for logging and registry entry."""
return {
"strategy_id": self.config.strategy_id,
"period": f"{self.config.start_date} to {self.config.end_date}",
"config_hash": self.config.config_hash(),
"metrics": {
"total_return_pct": round(self.total_return * 100, 2),
"sharpe": round(self.sharpe_ratio, 3),
"sortino": round(self.sortino_ratio, 3),
"max_drawdown_pct": round(self.max_drawdown * 100, 2),
"drawdown_duration_days": self.max_drawdown_duration_days,
"win_rate": round(self.win_rate * 100, 2),
"profit_factor": round(self.profit_factor, 3),
"num_trades": self.num_trades,
},
"generated_at": datetime.utcnow().isoformat(),
}
def validate(self) -> list[str]:
"""Run minimum threshold checks. Returns list of critical warnings."""
warnings = []
if self.sharpe_ratio < 1.0:
warnings.append(f"CRITICAL: Sharpe ratio {self.sharpe_ratio:.3f} below minimum 1.0")
if self.win_rate < 0.45:
warnings.append(f"CRITICAL: Win rate {self.win_rate:.1%} below minimum 45%")
if self.max_drawdown > 0.20:
warnings.append(f"CRITICAL: Max drawdown {self.max_drawdown:.1%} exceeds 20% limit")
if self.num_trades < 500:
warnings.append(f"WARNING: Only {self.num_trades} trades — consider extending the backtest period")
return warnings
def load_historical_data(symbol: str, start: str, end: str) -> dict:
"""Load historical OHLCV data for backtesting.
Uses TickDB kline endpoint. For US equities, data spans 10+ years
with cleaned and aligned bars. Historical data is suitable for
strategy backtesting, NOT for live trading.
Args:
symbol: Ticker symbol (e.g., "AAPL.US")
start: Start date in ISO format
end: End date in ISO format
Returns:
Dictionary with OHLCV data aligned to the specified interval
"""
import requests
api_key = os.environ.get("TICKDB_API_KEY")
if not api_key:
raise EnvironmentError("TICKDB_API_KEY environment variable not set")
response = requests.get(
"https://api.tickdb.ai/v1/market/kline",
headers={"X-API-Key": api_key},
params={
"symbol": symbol,
"interval": "1h", # Adjust interval as needed
"start": start,
"end": end,
"limit": 1000,
},
timeout=(3.05, 10),
)
if response.status_code == 200:
data = response.json()
if data.get("code") == 0:
return data.get("data", {})
elif data.get("code") == 2002:
raise ValueError(f"Symbol {symbol} not found — check via /v1/symbols/available")
else:
raise RuntimeError(f"API error {data.get('code')}: {data.get('message')}")
raise RuntimeError(f"HTTP {response.status_code} when fetching {symbol}")
# Usage example for a momentum reversal strategy
if __name__ == "__main__":
config = BacktestConfig(
strategy_id="MOM-REVERSAL-001",
start_date="2021-01-01",
end_date="2024-12-31",
initial_capital=1_000_000.0,
)
print(f"Config hash: {config.config_hash()}")
print(f"Cost model: {config.commission} per share, {config.slippage_bps} bps slippage")
# Load data
try:
data = load_historical_data("AAPL.US", config.start_date, config.end_date)
print(f"Loaded {len(data.get('bars', []))} bars for AAPL.US")
except Exception as e:
print(f"Data loading failed: {e}")
The key principle here: configuration is versioned. The config_hash() method generates a unique fingerprint for every backtest configuration. When a strategy's performance changes, the first question is always "did the config change?" This eliminates the "it worked before but I do not know what changed" failure mode.
Phase 4: Paper Trading — Production-Equivalent Code
Paper trading is not a test of the strategy logic. It is a test of the execution pipeline.
The code that runs in paper trading must be the same code that runs in production — byte-for-byte identical. The only difference is the API endpoint and the fact that no real capital is at risk.
This means:
- The same WebSocket connection handling.
- The same heartbeat and reconnection logic.
- The same order routing and fill simulation logic.
- The same position tracking and PnL calculation.
Production-Grade WebSocket Template for Paper and Live Trading
"""
Real-time market data subscriber for paper and live trading.
Team standard: all production strategies must use this connection handler.
⚠️ For production HFT workloads, replace requests with aiohttp/asyncio
and implement a dedicated connection pool.
"""
import os
import json
import time
import random
import threading
import logging
from datetime import datetime
from typing import Callable, Optional
import websocket # pip install websocket-client
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger("tickdb_subscriber")
class TickDBSubscriber:
"""
WebSocket subscriber for real-time TickDB market data.
Features:
- Automatic reconnection with exponential backoff + jitter
- Heartbeat (ping/pong) for connection health
- Rate-limit handling (3001 error code)
- Thread-safe message processing
- Graceful shutdown
Usage:
def on_depth(data):
print(f"Depth update: {data}")
subscriber = TickDBSubscriber(on_depth=on_depth)
subscriber.subscribe_depth("AAPL.US")
subscriber.run() # Blocking — run in thread or asyncio loop
"""
BASE_URL = "wss://api.tickdb.ai/ws/market"
PING_INTERVAL = 30 # seconds
def __init__(
self,
on_depth: Optional[Callable] = None,
on_trade: Optional[Callable] = None,
on_error: Optional[Callable] = None,
):
self.api_key = os.environ.get("TICKDB_API_KEY")
if not self.api_key:
raise EnvironmentError("TICKDB_API_KEY environment variable not set")
self.ws: Optional[websocket.WebSocketApp] = None
self._running = False
self._reconnect_delay = 1.0
self._max_delay = 60.0
self._retry_count = 0
# Callbacks
self.on_depth = on_depth
self.on_trade = on_trade
self.on_error = on_error or (lambda e: logger.error(f"WS error: {e}"))
# Thread safety
self._lock = threading.Lock()
self._subscriptions = set()
def _connect(self):
"""Establish WebSocket connection with API key as URL parameter."""
url = f"{self.BASE_URL}?api_key={self.api_key}"
self.ws = websocket.WebSocketApp(
url,
on_message=self._on_message,
on_error=self._on_error,
on_close=self._on_close,
on_open=self._on_open,
)
logger.info(f"Connecting to TickDB WebSocket...")
self._running = True
self.ws.run_forever(ping_interval=self.PING_INTERVAL)
def _on_open(self, ws):
"""Called when WebSocket connection is established."""
logger.info("WebSocket connection established")
self._reconnect_delay = 1.0 # Reset backoff on successful connection
self._retry_count = 0
# Re-subscribe to any channels that were active before disconnect
with self._lock:
for sub in self._subscriptions:
self._send_subscribe(sub)
def _on_message(self, ws, message: str):
"""Process incoming messages from TickDB."""
try:
data = json.loads(message)
# Handle error codes
if "code" in data and data["code"] != 0:
self._handle_error(data)
return
# Route by channel type
channel = data.get("channel", "")
if channel == "depth" and self.on_depth:
self.on_depth(data.get("data", {}))
elif channel == "trades" and self.on_trade:
self.on_trade(data.get("data", {}))
except json.JSONDecodeError:
logger.warning(f"Received non-JSON message: {message[:100]}")
except Exception as e:
logger.error(f"Error processing message: {e}")
def _handle_error(self, data: dict):
"""Handle TickDB error codes with appropriate action."""
code = data.get("code", 0)
message = data.get("message", "Unknown error")
if code == 3001:
# Rate limit — read Retry-After header if available
retry_after = int(data.get("headers", {}).get("Retry-After", 5))
logger.warning(f"Rate limited (3001). Waiting {retry_after}s before retry.")
time.sleep(retry_after)
else:
logger.error(f"TickDB error {code}: {message}")
if self.on_error:
self.on_error(data)
def _on_error(self, ws, error):
"""Handle WebSocket-level errors."""
logger.error(f"WebSocket error: {error}")
if self.on_error:
self.on_error(str(error))
def _on_close(self, ws, close_status_code, close_msg):
"""Handle connection close — triggers reconnection logic."""
logger.warning(f"Connection closed (code={close_status_code})")
self._running = False
self._schedule_reconnect()
def _schedule_reconnect(self):
"""Schedule reconnection with exponential backoff + jitter."""
if not self._running:
return
# Exponential backoff: delay = min(base × 2^retry, max_delay)
delay = min(self._reconnect_delay * (2 ** self._retry_count), self._max_delay)
# Add jitter: random uniform(0, delay × 0.1) to prevent thundering herd
jitter = random.uniform(0, delay * 0.1)
total_delay = delay + jitter
logger.info(f"Reconnecting in {total_delay:.1f}s (attempt {self._retry_count + 1})")
time.sleep(total_delay)
self._retry_count += 1
self._reconnect_delay = delay # Cap at current delay for next iteration
def _send_subscribe(self, subscription: dict):
"""Send subscription message to TickDB."""
if self.ws:
self.ws.send(json.dumps({"cmd": "subscribe", **subscription}))
def subscribe_depth(self, symbol: str, levels: int = 5):
"""
Subscribe to order book depth updates for a symbol.
Args:
symbol: Ticker symbol (e.g., "AAPL.US")
levels: Number of price levels (1-10 for HK/crypto, 1 for US equities)
"""
sub = {"channel": "depth", "symbol": symbol, "params": {"levels": levels}}
with self._lock:
self._subscriptions.add(frozenset(sub.items()))
self._send_subscribe(sub)
logger.info(f"Subscribed to depth: {symbol} ({levels} levels)")
def subscribe_trades(self, symbol: str):
"""Subscribe to trade stream for a symbol."""
sub = {"channel": "trades", "symbol": symbol}
with self._lock:
self._subscriptions.add(frozenset(sub.items()))
self._send_subscribe(sub)
logger.info(f"Subscribed to trades: {symbol}")
def run(self):
"""Start the subscriber — blocks indefinitely."""
while True:
try:
self._connect()
except Exception as e:
logger.error(f"Connection failed: {e}")
self._schedule_reconnect()
# Example: Real-time momentum signal monitor
def build_momentum_monitor(strategy_id: str, symbols: list[str]):
"""
Example: Monitor depth data for momentum signals.
This is the paper trading / live monitoring loop.
Replace the signal logic with your strategy's entry/exit rules.
"""
position_state = {} # symbol -> current position
def on_depth(data: dict):
"""Process depth update and generate signals."""
symbol = data.get("symbol")
bids = data.get("bids", [])
asks = data.get("asks", [])
if not bids or not asks:
return
# Calculate buy/sell pressure ratio
bid_volume = sum(float(b.get("size", 0)) for b in bids[:5])
ask_volume = sum(float(a.get("size", 0)) for a in asks[:5])
if ask_volume == 0:
return
pressure_ratio = bid_volume / ask_volume
# Example signal logic: extreme buy pressure
if pressure_ratio > 2.5:
logger.info(
f"[{strategy_id}] BUY SIGNAL: {symbol} "
f"pressure ratio {pressure_ratio:.2f} exceeds threshold"
)
position_state[symbol] = "long"
elif pressure_ratio < 0.4:
logger.info(
f"[{strategy_id}] SELL SIGNAL: {symbol} "
f"pressure ratio {pressure_ratio:.2f} below threshold"
)
position_state[symbol] = "short"
subscriber = TickDBSubscriber(on_depth=on_depth)
for symbol in symbols:
subscriber.subscribe_depth(symbol, levels=5)
return subscriber
if __name__ == "__main__":
import signal
# Graceful shutdown handler
def shutdown_handler(signum, frame):
logger.info("Shutdown signal received — stopping subscriber...")
exit(0)
signal.signal(signal.SIGINT, shutdown_handler)
signal.signal(signal.SIGTERM, shutdown_handler)
# Start monitoring a basket of stocks
monitor = build_momentum_monitor(
strategy_id="MOM-REVERSAL-001",
symbols=["AAPL.US", "MSFT.US", "GOOGL.US"],
)
logger.info("Starting real-time momentum monitor (paper trading mode)")
monitor.run()
This code establishes the minimum standard for all real-time data handling in the team's strategies. Key non-negotiable elements:
- Heartbeat via
ping_interval: The WebSocket connection is kept alive automatically. - Exponential backoff + jitter: Reconnection attempts space out to avoid hammering the API during outages.
- Rate-limit handling: Error code 3001 triggers an explicit wait using the
Retry-Afterheader. - Thread-safe subscription management: Multiple symbols can be subscribed without race conditions.
- Graceful shutdown: SIGINT and SIGTERM are handled to prevent orphaned connections.
Phase 5: Pre-Launch Review — The Deployment Checklist
Before any strategy enters production, it must pass a deployment review. The review is conducted by a second quant (not the strategy author) and covers six gates.
Deployment Checklist
| Gate | Item | Pass criteria | Owner |
|---|---|---|---|
| G1 | Backtest report | Complete, all metrics above minimum thresholds, no critical warnings | Strategy author |
| G2 | Backtest reproducibility | Independent reviewer can reproduce results with provided code and data | Reviewer |
| G3 | Cost model compliance | All cost assumptions match team standard; any variance documented and justified | Reviewer |
| G4 | Code review | No hardcoded credentials, no blocking I/O in hot path, timeout on all external calls | Senior engineer |
| G5 | Risk limits | Position limits, drawdown stops, and correlation checks implemented and tested | Risk officer |
| G6 | Monitoring active | Alerts configured for: connection loss, drawdown breach, signal frequency anomaly | Ops team |
The deployment checklist is not a suggestion. A strategy without a signed checklist cannot be deployed. The signed checklist is stored in the strategy registry alongside the backtest report.
Phase 7: Live Monitoring — The Operational Dashboard
Once in production, every strategy requires active monitoring. The monitoring system serves three functions:
- Detect silent failures: Connection drops, data gaps, and execution errors that do not produce visible exceptions.
- Detect performance drift: Strategy performance deviating from expected ranges.
- Generate audit logs: Complete records of all signals, orders, and position changes for attribution analysis.
Minimum Monitoring Metrics
| Metric | Alert threshold | Action |
|---|---|---|
| Connection uptime | < 99.5% in 24h | Page ops team immediately |
| Signal frequency | > 3σ deviation from historical mean | Suspend strategy pending review |
| Daily PnL | < −2σ from expected | Page quant lead |
| Drawdown | > −15% from peak | Auto-close positions; page risk officer |
| Fill rate | < 95% of expected fills | Suspend execution pending review |
Data Source for Monitoring
For real-time monitoring, the team's strategies consume market data via TickDB WebSocket. The monitoring dashboard should ingest:
- Depth data: Order book pressure ratios, spread widening events.
- Trade data: Fill confirmation, order rejections.
- Strategy logs: Signal generation events, position state changes.
The WebSocket subscriber template provided above already includes logging for connection events. Extending it to emit custom metrics for a Prometheus or Grafana dashboard is a standard exercise left to the team's observability stack.
Organizing the Team's Strategy Portfolio
With a standardized pipeline in place, the strategy registry becomes a genuine asset rather than a graveyard of abandoned ideas.
Quarterly Portfolio Review
Every quarter, the team conducts a formal portfolio review. The review covers:
- Performance attribution: Which strategies are contributing to returns? Which are drag?
- Correlation analysis: Are any strategies too correlated with each other? Should positions be combined?
- Capacity headroom: Which strategies are approaching their AUM capacity limit?
- Pipeline health: How many strategies are in each phase? Are any stuck in backtesting for more than 6 weeks?
Strategies that have not generated a signal in 30 days or that have a Sharpe ratio below 0.5 for two consecutive quarters are flagged for reauthorization review.
Implementation Roadmap
Building this pipeline is not a weekend project. A realistic rollout for a team of five to eight quants follows this sequence:
| Week | Deliverable | Owner |
|---|---|---|
| 1–2 | Strategy registry template and access controls | Team lead |
| 3–4 | Standardized cost model agreed and documented | All quants |
| 5–6 | Backtest template distributed; existing strategies migrated | Individual quants |
| 7–8 | WebSocket subscriber template reviewed and adopted | Engineering lead |
| 9–10 | First deployment checklist signed for a pilot strategy | Pilot quant + reviewer |
| 11–12 | Full pipeline operational; monitoring dashboard live | Ops + engineering |
| 13+ | Quarterly review cadence established | Team lead |
The goal is not perfection on day one. The goal is consistency. A pipeline that produces "good enough" output reliably is far more valuable than a bespoke process that occasionally produces excellence.
Next Steps
For individual quants: Audit your current backtests against the cost model standards in this article. If you find variance, document it and bring it to the next team meeting.
For team leads: Introduce the strategy registry template in the next sprint. Assign a reviewer for the first deployment checklist within two weeks.
For operations: Deploy the WebSocket subscriber template in a staging environment and verify that reconnection logic handles simulated network interruptions correctly.
The pipeline described here is framework-agnostic. It works whether you trade equities, futures, or crypto — the cost model parameters and asset-class specifics adjust, but the phase structure, review gates, and code standards remain constant.
This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results. All backtest results are based on historical simulation and are subject to the limitations described in the backtest disclosure standards above.