The Moment Everything Changes
You built a strategy. It works. You were alone.
Then you brought in a partner — a former colleague who writes cleaner code than you and has ideas of their own. Suddenly, your main.py that lives on your laptop becomes a coordination problem. Whose version is live? Who pushed the broken backtest? Where is the clean dataset that Chen spent three hours cleaning last week?
Three-person quantitative teams face a specific kind of infrastructure crisis: they are complex enough that coordination breaks, but not large enough to have a dedicated DevOps engineer. The solution is not more people. The solution is a data infrastructure that scales with the work, not the headcount.
This article walks through the production-grade infrastructure stack a three-person quant team needs — covering shared data access, version-controlled code repositories, API key management, and permission controls. Every recommendation is concrete, implements in under two hours, and survives contact with a live trading system.
The Three Collaboration Failure Modes
Before designing the system, it helps to name the failure modes that kill small quant teams.
Failure Mode 1: Data Silos
Each team member maintains their own copy of market data. The US equity OHLCV dataset lives on your workstation. Chen has a copy on a NAS drive. Marcus pulls fresh data every time he runs a backtest. Results diverge not because the strategy changed, but because the data did.
Failure Mode 2: Code Drift
You refactored signal_engine.py last Tuesday. Chen is still running the version from two weeks ago — the one with the bug you thought you fixed. Backtest results do not match. Nobody can reproduce the live system's behavior.
Failure Mode 3: Credential Leakage
Marcus needs to call the market data API from his research script. You share the API key over Slack. Three months later, the key is in a GitHub repository that was accidentally made public. The credential is burned. You rotate it. Chen's scheduled job breaks. Nobody knows why until market open.
These are not hypothetical. They are the three most common infrastructure failures in sub-five-person quant teams. The fix for each is architectural, not disciplinary.
Infrastructure Architecture: The Four Pillars
A resilient shared infrastructure for a small quant team rests on four pillars. Each pillar has a specific purpose and a recommended implementation.
| Pillar | Purpose | Recommended Tool | Cost |
|---|---|---|---|
| Shared data storage | Single source of truth for market data | S3-compatible storage + TickDB for live feeds | $20–$80/month |
| Version control | Reproducible code and backtest history | GitHub private repository | Free (free tier) |
| Secrets management | Secure storage and rotation of API keys | HashiCorp Vault or AWS Secrets Manager | $0–$50/month |
| Access control | Per-user permissions and audit logging | GitHub Teams + Cloud IAM | Included in above |
The architecture ties together as follows: TickDB provides the live and historical market data feeds. Raw and processed datasets are stored in S3-compatible storage. All strategy code lives in a private GitHub repository with branch protection. API keys and database credentials are stored in a secrets manager, never in code or Slack messages. Access is governed by GitHub Teams and cloud IAM roles.
Pillar 1: Shared Data Storage with TickDB
The Problem with Local Data Copies
The most dangerous assumption a quant team makes is that a dataset is static. It is not. Exchange APIs evolve. Data vendors update their normalization logic. A dataset that was "good enough" last quarter may have subtle changes that invalidate a six-month backtest run today.
TickDB solves this by providing a unified, timestamped feed of historical and live market data through a consistent REST and WebSocket API. When your team pulls from a single endpoint, every backtest references the same canonical dataset.
Setting Up Shared Access
For a three-person team, the recommended approach is a shared TickDB account with individual API keys per user. This preserves auditability — you can see which team member's code consumed how much quota — while enabling centralized billing.
Each team member generates their own API key from the TickDB dashboard. The keys are not shared. They are stored in the secrets manager (covered in Pillar 3).
Fetching Historical Data for Shared Storage
The following Python script demonstrates a production-grade data fetcher that pulls historical OHLCV data from TickDB, stores it in a local S3-compatible path, and includes retry logic with exponential backoff.
import os
import json
import time
import hashlib
import logging
from datetime import datetime, timedelta
from pathlib import Path
import requests
# ⚠️ API key loaded from environment — never hardcoded
TICKDB_API_KEY = os.environ.get("TICKDB_API_KEY")
if not TICKDB_API_KEY:
raise ValueError("TICKDB_API_KEY environment variable is not set")
BASE_URL = "https://api.tickdb.ai/v1"
HEADERS = {"X-API-Key": TICKDB_API_KEY}
DATA_ROOT = Path(os.environ.get("TICKDB_DATA_ROOT", "./data"))
LOG_FILE = Path(os.environ.get("LOG_FILE", "./fetch.log"))
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
handlers=[logging.FileHandler(LOG_FILE), logging.StreamHandler()],
)
logger = logging.getLogger(__name__)
def fetch_kline(symbol: str, interval: str, start_time: int, end_time: int, max_retries: int = 3):
"""
Fetch historical kline (OHLCV) data from TickDB.
Args:
symbol: Exchange symbol, e.g. "AAPL.US"
interval: Candle interval, e.g. "1h", "1d"
start_time: Unix timestamp (ms) for range start
end_time: Unix timestamp (ms) for range end
max_retries: Maximum retry attempts on failure
Returns:
List of kline records
"""
url = f"{BASE_URL}/market/kline"
params = {
"symbol": symbol,
"interval": interval,
"start_time": start_time,
"end_time": end_time,
"limit": 1000, # Maximum allowed per request
}
all_records = []
retry_count = 0
base_delay = 2.0
while retry_count <= max_retries:
try:
response = requests.get(
url,
headers=HEADERS,
params=params,
timeout=(3.05, 15), # (connect timeout, read timeout)
)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", base_delay))
logger.warning(f"Rate limited. Retrying after {retry_after}s")
time.sleep(retry_after)
continue
response.raise_for_status()
data = response.json()
if data.get("code") != 0:
error_code = data.get("code")
error_msg = data.get("message", "Unknown error")
# Handle rate limit within response body
if error_code == 3001:
retry_after = int(data.get("retry_after", base_delay))
logger.warning(f"Rate limited (code 3001). Retrying after {retry_after}s")
time.sleep(retry_after)
continue
raise RuntimeError(f"API error {error_code}: {error_msg}")
records = data.get("data", {}).get("klines", [])
all_records.extend(records)
logger.info(f"Fetched {len(records)} records for {symbol} @ {interval}")
return all_records
except requests.exceptions.Timeout:
retry_count += 1
delay = base_delay * (2 ** retry_count) + time.uniform(0, 0.5)
logger.warning(f"Request timed out. Retry {retry_count}/{max_retries} in {delay:.1f}s")
time.sleep(delay)
except requests.exceptions.RequestException as e:
retry_count += 1
delay = base_delay * (2 ** retry_count) + time.uniform(0, 0.5)
logger.error(f"Request failed: {e}. Retry {retry_count}/{max_retries} in {delay:.1f}s")
time.sleep(delay)
raise RuntimeError(f"Failed to fetch data after {max_retries} retries")
def store_data_locally(symbol: str, interval: str, records: list, data_root: Path):
"""
Store fetched kline records in a structured local directory.
File naming: {symbol}_{interval}_{YYYYMMDD}.json
"""
if not records:
logger.warning(f"No records to store for {symbol} @ {interval}")
return
first_ts = records[0].get("t", 0)
date_str = datetime.fromtimestamp(first_ts / 1000).strftime("%Y%m%d")
filename = f"{symbol.replace('/', '_')}_{interval}_{date_str}.json"
filepath = data_root / symbol.replace("/", "_") / interval / filename
filepath.parent.mkdir(parents=True, exist_ok=True)
with open(filepath, "w") as f:
json.dump({
"symbol": symbol,
"interval": interval,
"fetched_at": datetime.utcnow().isoformat(),
"record_count": len(records),
"records": records,
}, f, indent=2)
logger.info(f"Stored {len(records)} records to {filepath}")
def compute_data_hash(records: list) -> str:
"""Compute SHA-256 hash of records for integrity verification."""
content = json.dumps(records, sort_keys=True)
return hashlib.sha256(content.encode()).hexdigest()[:16]
def fetch_and_store_symbol(symbol: str, interval: str, days_back: int = 365):
"""
Main workflow: fetch historical data and store to local path.
"""
end_time_ms = int(datetime.utcnow().timestamp() * 1000)
start_time_ms = int((datetime.utcnow() - timedelta(days=days_back)).timestamp() * 1000)
logger.info(f"Fetching {symbol} @ {interval} from {days_back} days ago")
records = fetch_kline(symbol, interval, start_time_ms, end_time_ms)
if records:
store_data_locally(symbol, interval, records, DATA_ROOT)
record_hash = compute_data_hash(records)
logger.info(f"Data integrity hash (first 16 chars): {record_hash}")
return records
if __name__ == "__main__":
# Example: fetch AAPL daily data for the past year
symbols = ["AAPL.US", "NVDA.US", "SPY.US"]
interval = "1d"
for symbol in symbols:
try:
fetch_and_store_symbol(symbol, interval, days_back=365)
except Exception as e:
logger.error(f"Failed to fetch {symbol}: {e}")
Why This Code Is Production-Grade
The fetcher above is not a toy script. It handles the failure modes that appear in real deployment:
- Timeout handling: Every request has a
(3.05, 15)tuple timeout — 3.05 seconds to establish connection, 15 seconds to read the response. The 3.05 second connect timeout is intentional — it exceeds the 3-second mark that load balancers use to drop slow connections. - Exponential backoff with jitter: On failure, the script doubles the wait time between retries and adds random jitter (0–0.5 seconds) to prevent thundering herd behavior when the API recovers.
- Rate limit handling: The script checks both HTTP 429 status codes and TickDB's internal
code: 3001response field. It reads theRetry-Afterheader and pauses accordingly. - Integrity hashing: A SHA-256 hash of each dataset is logged. When two team members compare backtest results, they can verify they are running against identical data.
Pillar 2: Git Workflow for Quantitative Code
The Branching Strategy
For a three-person quant team, a simple branching strategy beats a complex one. Use a trunk-based development model with short-lived feature branches.
main (protected)
└── feat/signal-refactor
└── feat/new-factor
└── hotfix/data-pipeline-fix
mainis protected. No direct pushes. All changes go through pull requests with at least one required reviewer.- Feature branches live for 1–3 days. Longer-lived branches diverge and create merge nightmares.
- Commits are atomic. Each commit should represent one logical change — a new factor, a bug fix, a data pipeline update.
Commit Message Convention
Adopt a conventional commit format that makes git log readable and enables automated changelog generation.
feat: add mean-reversion factor based on z-score crossover
fix: correct timestamp alignment in futures data pipeline
data: update AAPL dividend adjustments for Q1 2025
backtest: add transaction cost model with 0.5 bps per trade
infra: configure GitHub Actions for nightly backtest run
Backtest Reproducibility via Git Tags
Tag every backtest run with a commit hash. This is non-negotiable for team reproducibility.
# Run the backtest
python backtest.py --config configs/earnings_reversion.yaml
# Tag the results with the current commit
git add results/backtest_2025_02_15.json
git commit -m "backtest: earnings reversion run for Feb 2025 earnings season"
git tag -a backtest/earnings_2025q1 -m "Full backtest of earnings reversion strategy, Q1 2025"
git push origin main --tags
When Marcus asks why his results differ from Chen's, the answer is one git log command away.
GitHub Actions for Automated Backtesting
Set up a nightly pipeline that runs the canonical backtest suite and publishes results as a GitHub Actions artifact.
# .github/workflows/nightly_backtest.yml
name: Nightly Backtest
on:
schedule:
- cron: "0 6 * * *" # Run at 6 AM UTC daily
workflow_dispatch: # Allow manual trigger
jobs:
backtest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Run backtest suite
run: python -m pytest tests/backtest_suite.py --junitxml=results/junit.xml
env:
TICKDB_API_KEY: ${{ secrets.TICKDB_API_KEY }}
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: backtest-results-${{ github.run_number }}
path: results/
Pillar 3: API Key and Secrets Management
The Vault Architecture
Never store API keys in environment files, code, or chat messages. For a three-person team, HashiCorp Vault in dev mode is the right balance of security and operational simplicity.
Vault runs locally on each developer's machine (or on a shared lab server) and provides:
- Centralized secrets storage: All API keys, database passwords, and cloud credentials live in one place.
- Dynamic credentials: Vault can generate temporary AWS or database credentials that expire automatically.
- Audit logging: Every secret access is logged with the requesting user and timestamp.
Setting Up Vault (Dev Mode)
# Start Vault in dev mode (not for production use — dev only)
vault server -dev -dev-root-token-id="dev-token"
# In another terminal, set up a path for team secrets
export VAULT_ADDR="http://127.0.0.1:8200"
export VAULT_TOKEN="dev-token"
# Store a TickDB API key
vault kv put secret/tickdb/main \
api_key="tk_live_xxxxxxxxxxxxxxxxxxxx" \
owner="chen@example.com" \
created_at="$(date -u +%Y-%m-%d)"
# Store a second key for Marcus
vault kv put secret/tickdb/marcus \
api_key="tk_live_yyyyyyyyyyyyyyyyyyyyy" \
owner="marcus@example.com" \
created_at="$(date -u +%Y-%m-%d)"
# Verify
vault kv get secret/tickdb/main
Application Integration
Your Python code reads keys from Vault at startup, never from a hardcoded string.
import hvac
import os
def get_tickdb_credentials():
"""
Retrieve TickDB API key from HashiCorp Vault.
Falls back to environment variable for CI/CD pipelines.
"""
vault_addr = os.environ.get("VAULT_ADDR")
vault_token = os.environ.get("VAULT_TOKEN")
if vault_addr and vault_token:
client = hvac.Client(url=vault_addr, token=vault_token)
secret = client.secrets.kv.v2.read_secret_version(
path="secret/tickdb/main",
mount_point="secret",
)
return secret["data"]["data"]["api_key"]
# CI/CD fallback: use CI environment variable
api_key = os.environ.get("TICKDB_API_KEY")
if not api_key:
raise ValueError("No TickDB credentials found. Set VAULT_ADDR/VAULT_TOKEN or TICKDB_API_KEY.")
return api_key
API Key Rotation Protocol
When a key is compromised, rotation must be fast and auditable.
- Generate a new key from the TickDB dashboard.
- Update the new key in Vault:
vault kv patch secret/tickdb/main api_key="tk_live_NEWKEY..." - Trigger a restart of any running processes that read from Vault. Dynamic secret providers like Vault automatically cache credentials for a configured TTL (time-to-live). Set the TTL to 5 minutes maximum for API keys.
- Revoke the old key from the TickDB dashboard immediately after confirming the new key is operational.
Pillar 4: Permission Control
GitHub Teams Structure
For a three-person team, three GitHub Teams map cleanly to roles:
| Team | Members | Permissions |
|---|---|---|
@quant-team/admin |
Team lead | Full repository access, can merge PRs, manage settings |
@quant-team/researchers |
Chen, Marcus | Read/write on main repository, cannot modify branch protection |
@quant-team/observers |
External advisor | Read-only access, can comment on PRs but not push |
Data Access Permissions
Not every team member needs access to every dataset. If your team works across multiple strategies, assign data access based on the strategy a user is actively contributing to.
TickDB's API key system supports this naturally: generate per-user keys and configure separate rate limit pools. An API key assigned to "earnings strategy" has no additional privilege over a key assigned to "futures arbitrage" — but the audit trail tells you exactly which team's code consumed which quota.
Secrets Manager Permissions
In Vault, use policies to restrict access:
# researcher-policy.hcl
path "secret/data/tickdb/*" {
capabilities = ["read"]
}
path "secret/data/strategies/*" {
capabilities = ["read"]
}
path "secret/data/infrastructure/*" {
capabilities = []
}
This policy gives a researcher read access to data keys but no access to infrastructure credentials (cloud keys, database passwords). Apply the policy to Chen's and Marcus's Vault tokens.
Deployment Guide by Team Stage
The infrastructure described above adapts as the team grows. Here is a deployment roadmap by team size and complexity.
| Stage | Team size | Key additions | Remove or upgrade |
|---|---|---|---|
| Stage 1: Foundation | 1–2 | GitHub private repo, Vault dev mode, shared S3 bucket | None |
| Stage 2: Collaboration | 3–4 | GitHub Teams with branch protection, separate API keys per user, scheduled GitHub Actions backtests | Local Vault → Vault server |
| Stage 3: Production | 5–10 | Vault server with HA, IAM roles for cloud resources, separate strategy repositories, on-call rotation | Shared repo → per-strategy repos |
For your three-person team, Stage 2 is the target. Do not prematurely adopt Stage 3 complexity.
Putting It Together: The Shared Workflow
The four pillars work together as a single workflow that every team member follows.
1. Pull latest code
git checkout main && git pull
2. Fetch latest data to shared storage
python scripts/fetch_data.py --symbol AAPL.US --interval 1d
3. Run backtest
python backtest.py --config configs/earnings_reversion.yaml
4. Commit results with tag
git add results/ && git commit -m "backtest: reversion strategy, $(date +%Y%m%d)"
git tag backtest/reversion_$(date +%Y%m%d)
5. Push and create PR
git push origin main --tags
# Open PR in GitHub — requires at least one reviewer's approval
If a team member's backtest produces different results, the diagnostic steps are:
- Confirm both are on the same commit:
git log --oneline - Confirm both are using the same dataset hash: check the logged hash from the fetcher
- Confirm both are using the same environment variables:
diff <(env | sort) <(other_user_env | sort)
What Not to Build
A three-person team should resist the urge to over-engineer. The following are premature optimizations for a team of your size:
- Custom data pipelines: Use TickDB's REST and WebSocket APIs directly. Building a custom ingestion layer adds maintenance burden with zero benefit at this scale.
- Multi-cloud architecture: Pick one cloud provider. Running AWS for some strategies and GCP for others creates data alignment nightmares.
- Microservices: Keep the codebase in one monorepo. Microservice communication overhead is not worth it for three people managing a handful of strategies.
- Real-time trading infrastructure: Until you have a strategy with a validated backtest and positive live track record over at least 60 days, paper trading with shared dashboards is sufficient.
Next Steps
If you're evaluating market data infrastructure for your team, visit tickdb.ai to explore the API documentation, generate a free API key with no credit card required, and review the coverage matrix for your target asset classes.
If you're building a shared data pipeline, the code examples in this article are directly usable as starting points. Clone the GitHub repository, configure your Vault secrets, and replace the symbol list with your target universe.
If you need institutional-grade historical data for cross-cycle backtesting, reach out to enterprise@tickdb.ai for plans that include 10+ years of cleaned US equity OHLCV data, dedicated rate limit pools, and SLA-backed uptime guarantees.
If you're an AI-assisted developer, search for the tickdb-market-data SKILL in your AI tool's marketplace. It provides pre-built function calls for kline, depth, and trades endpoints — reducing the time to integrate TickDB into your research workflow.
This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results. Infrastructure decisions should align with your team's regulatory obligations and operational capacity.