At 09:30:01, the backtest places a buy order at 101.25. The live system submits the same order thirty-one milliseconds later and receives a fill at 101.34.
Nothing appears broken. The order was accepted, the position opened, and the risk limit passed. Yet the strategy that looked profitable in simulation has surrendered most of its expected edge before the market has even moved.
The backtest-to-live problem is rarely one hidden defect. It is usually a stack of mismatches: unrealistically favorable fills, stale market data, unmodeled latency, fragile connection handling, statistical overfitting, and inconsistent execution under pressure.
The answer is not “make the backtest more complicated.” The answer is to measure the same decision path in both environments. For each order, you must know the information available at decision time, the expected execution price, the realized fill, the latency, the connection state, and the reason the system acted.
This article provides a practical framework for closing that gap across five failure modes, with a production-oriented monitoring implementation for slippage, latency, and disconnects.
1. Diagnose the Failure Before Changing the Strategy
A backtest is a controlled experiment over historical observations. Live trading is a distributed system operating against an adversarial market. The two environments differ in more than execution.
Consider an illustrative deployment diagnostic. The numbers below describe a fictional system and are not reported strategy performance.
| Failure dimension | Backtest assumption | Illustrative live observation | Likely consequence |
|---|---|---|---|
| Slippage | 1 basis point per side | 7.5 basis points per side | Expected edge becomes negative after costs |
| Decision-to-order latency | 25 ms | 180 ms | Orders arrive after the signal has weakened |
| Disconnects | None modeled | Three interruptions in one week | Stale state, duplicate orders, missed exits |
| Statistical validation | 320 out-of-sample trades | 28 live trades | Live sample is too small to diagnose regime dependence |
| Rule adherence | 100% | 72% | Manual overrides alter the tested strategy |
Each row represents a different engineering or research discipline. Fixing one does not fix the others.
The correct migration strategy therefore has two parallel tracks:
- Parity engineering makes the live decision path observable and reproducible.
- Statistical validation determines whether the strategy survives conditions outside the historical sample.
You need both tracks. Production resilience cannot rescue an overfit strategy, and a well-tested strategy cannot survive an execution system that fills at the wrong time or loses connection during an exit.
2. Gap One: Slippage and Fill Assumptions
A backtest often models execution as either the next candle’s open, the current close, or a fixed slippage assumption. Live execution is a negotiation with the order book, spread, queue position, available size, venue rules, and concurrent order flow.
The first mistake is treating slippage as one constant. Real slippage depends on several conditions.
2.1 Model the decision price, not the final candle price
A backtest that fills at the close of the signal candle grants the strategy information it did not have when it placed the order. If a signal becomes available after the close, filling at that close is impossible.
Use this sequence:
- Record the market-data timestamp.
- Compute the signal.
- Record the decision timestamp.
- Submit the order.
- Record the exchange acknowledgement.
- Record each fill.
The execution model should use the first price available after step 2, adjusted for a defensible slippage and size model. It should not use a future bar close merely because that close appears in the downloaded dataset.
2.2 Separate spread cost from market impact
Spread cost and market impact create different effects.
| Cost component | Typical driver | Modeling approach |
|---|---|---|
| Spread crossing | Bid-ask spread at decision time | Half-spread for marketable orders |
| Latency slippage | Market movement during transport | Tick or bar movement over observed latency |
| Size impact | Order quantity relative to available depth | Queue or volume participation model |
| Venue effects | Exchange, routing, and fee structure | Per-venue execution model |
| Reject or retry cost | Invalid or stale order state | Explicit operational loss scenario |
For market orders, a conservative estimate is:
[
\text{Expected Fill} =
\text{Decision Reference Price}
- \text{Spread Cost}
- \text{Latency Adjustment}
- \text{Size Impact}
]
For limit orders, the model must also account for non-fill probability and adverse selection. A limit order that fills only when the market moves against you may be less attractive than a lower-fill-rate market order.
2.3 Validate the assumption against live fills
The production tolerance should be based on measured slippage distribution, not a universal number. Track at least:
- Mean and median realized slippage.
- Standard deviation and tail percentiles.
- Slippage by side.
- Slippage by time of day.
- Slippage by order type and size.
- Fill rate for limit orders.
- Spread at signal time versus spread at order arrival.
If the live median is seven basis points while the backtest assumes one basis point, recalibrate the model or remove the strategy. Do not merely widen the backtest result until it resembles live trading.
3. Gap Two: Latency and Stale State
Latency is not a single number. It is a chain of delays:
[
t_{\text{decision}}
\rightarrow t_{\text{serialize}}
\rightarrow t_{\text{network}}
\rightarrow t_{\text{broker}}
\rightarrow t_{\text{venue}}
\rightarrow t_{\text{ack}}
\rightarrow t_{\text{fill}}
]
The most dangerous latency often occurs before the order reaches the broker. If a market-data packet takes 300 milliseconds to arrive, the order may already be stale when the signal code begins processing it.
3.1 Measure timestamps from different clocks
A useful latency record contains at least four timestamps:
| Timestamp | Source | Purpose |
|---|---|---|
| Event timestamp | Market-data provider or venue | When the market event occurred |
| Receive timestamp | Your process | When the packet arrived |
| Decision timestamp | Your strategy | When the signal became actionable |
| Order timestamp | Broker or venue | When the order was accepted |
| Fill timestamp | Broker or venue | When execution occurred |
Provider event time, local receive time, and broker time may use different clock sources. Synchronize them with a monitoring service such as NTP and measure offset separately. Without clock discipline, a latency dashboard can confidently report impossible values.
3.2 Age data before using it
Do not process a market event solely because it arrived. Check its age:
max_event_age_ms = 250
if event_age_ms > max_event_age_ms:
logger.warning(
"Discarding stale market event",
extra={
"symbol": symbol,
"event_age_ms": event_age_ms,
"max_event_age_ms": max_event_age_ms,
},
)
return None
For fast signals, stale data may need to invalidate the position model rather than merely trigger a warning. A live system can know that its assumptions are invalid only if it records enough state to detect the change.
3.3 Compare latency with signal half-life
A 100-millisecond delay may be irrelevant for a daily strategy and fatal for a short-horizon signal. Measure how quickly the expected edge decays after the signal appears.
If a signal’s expected movement falls below estimated transaction cost after 80 milliseconds, an architecture with a 180-millisecond path is unsuitable. No amount of broker optimization compensates for entering after the opportunity has expired.
3.4 Use the right historical endpoint
For completed research periods, use:
GET /v1/market/kline
For a current real-time candle, use:
GET /v1/market/kline/latest
Using the completed-period endpoint for a live dashboard or the latest endpoint for historical backtesting mixes incompatible semantics. The former can include data that was not available during the historical decision point; the latter does not replace a reproducible historical series.
4. Gap Three: Disconnects and Recovery Semantics
Most trading systems do not fail because the network disappears. They fail because the application cannot determine what happened while the network disappeared.
Consider this sequence:
- The strategy sends a buy order.
- The broker receives the order.
- The network disconnects before the client receives the acknowledgement.
- The strategy reconnects and assumes the order failed.
- The strategy sends another buy order.
- The broker fills both orders.
The original order was not lost. The client’s knowledge was lost. This is the classic uncertain-state problem, and reconnect logic must address it explicitly.
4.1 Make state transitions explicit
A resilient order state machine should distinguish at least these states:
| State | Meaning | Safe action |
|---|---|---|
CREATED |
Internal order object exists | Submit or prepare to submit |
SUBMITTING |
Send attempted; outcome unknown | Do not blindly resend |
ACKNOWLEDGED |
Broker accepted the order | Monitor until terminal state |
PARTIALLY_FILLED |
Some quantity executed | Track remaining quantity |
FILLED |
Target quantity completed | Update position and exposure |
CANCEL_PENDING |
Cancellation requested | Continue monitoring |
UNKNOWN |
Connection lost during a transition | Reconcile with broker state |
REJECTED |
Broker refused the order | Handle error according to policy |
CANCELLED |
Remaining quantity cancelled | Release reserved risk |
An order in UNKNOWN must never transition directly to a new submission. It must first be reconciled against broker state or a uniquely identifiable client order ID.
4.2 Detect event-sequence gaps
Broker event streams commonly provide sequence identifiers. If sequence 104 follows 101, the system missed events 102 and 103.
if previous_sequence is not None and sequence > previous_sequence + 1:
logger.error(
"Execution-event sequence gap",
extra={
"expected": previous_sequence + 1,
"received": sequence,
"missing": sequence - previous_sequence - 1,
},
)
reconcile_required = True
A sequence gap should trigger reconciliation, not just a log entry.
4.3 Use idempotent identifiers
Every order should carry a deterministic client order ID. If the strategy retries, it should reuse or reconcile the same logical request rather than create an unrelated order.
Idempotency requires both the broker and the internal system to preserve the association between:
- Client order ID.
- Broker order ID.
- Symbol and side.
- Submitted quantity.
- Current filled quantity.
- Average fill price.
Without durable order state, process restarts create the same ambiguity as network disconnects.
4.4 Reconnect with exponential backoff and jitter
A reconnect loop should not hammer the broker or create a synchronized retry storm across multiple instances.
delay = min(base_delay * (2 ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.1)
time.sleep(delay + jitter)
The recovery path should also:
- Re-authenticate when required.
- Reload account and position state.
- Reconcile open orders.
- Resume event consumption from a known sequence where supported.
- Disable new entries until reconciliation completes.
- Keep risk exits functional under stricter rules.
5. Build a Production Parity Monitor
The following Python example combines three elements:
- A resilient TickDB REST client for completed and current market data.
- A private execution-stream reader with heartbeat, exponential backoff, and jitter.
- An audit ledger that records slippage, latency, and sequence gaps.
The private-stream protocol is deliberately isolated because broker schemas differ. The normalized event requires these fields:
type, sequence, symbol, side, quantity, fill_price,
reference_price, event_timestamp, client_order_id
Adapt only the normalize_execution_event() function to the broker’s payload. The reconciliation, audit, and failure-handling behavior should remain independent of that schema.
import asyncio
import json
import logging
import os
import random
import time
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlencode, urlparse, parse_qsl, urlunparse
import requests
import websockets
logging.basicConfig(
level=os.getenv("LOG_LEVEL", "INFO"),
format="%(asctime)s %(levelname)s %(message)s",
)
logger = logging.getLogger("parity-monitor")
TICKDB_BASE_URL = os.getenv(
"TICKDB_BASE_URL", "https://api.tickdb.ai"
).rstrip("/")
TICKDB_API_KEY = os.environ.get("TICKDB_API_KEY")
TICKDB_SYMBOL = os.getenv("TICKDB_SYMBOL", "AAPL.US")
TICKDB_INTERVAL = os.getenv("TICKDB_INTERVAL", "1m")
PRIVATE_STREAM_URL = os.environ.get("PRIVATE_STREAM_URL")
PRIVATE_STREAM_TOKEN = os.environ.get("PRIVATE_STREAM_TOKEN")
MAX_SLIPPAGE_BPS = float(os.getenv("MAX_SLIPPAGE_BPS", "10"))
MAX_LATENCY_MS = float(os.getenv("MAX_LATENCY_MS", "250"))
AUDIT_PATH = Path(os.getenv("AUDIT_PATH", "execution_audit.jsonl"))
HTTP_TIMEOUT = (3.05, 10)
MAX_RATE_LIMIT_RETRIES = 5
if not TICKDB_API_KEY:
raise RuntimeError("TICKDB_API_KEY is required")
if not PRIVATE_STREAM_URL or not PRIVATE_STREAM_TOKEN:
raise RuntimeError(
"PRIVATE_STREAM_URL and PRIVATE_STREAM_TOKEN are required"
)
class TickDBAPIError(RuntimeError):
def __init__(self, code, message):
super().__init__(f"TickDB error {code}: {message}")
self.code = code
self.message = message
def tickdb_get(path, params):
"""Fetch TickDB REST data with timeout, auth, and rate-limit handling."""
url = f"{TICKDB_BASE_URL}{path}"
headers = {"X-API-Key": TICKDB_API_KEY}
for attempt in range(MAX_RATE_LIMIT_RETRIES):
response = requests.get(
url,
headers=headers,
params=params,
timeout=HTTP_TIMEOUT,
)
payload = {}
if response.content:
try:
payload = response.json()
except ValueError:
payload = {}
code = payload.get("code", 0)
is_rate_limited = response.status_code == 429 or code == 3001
if is_rate_limited:
retry_after = response.headers.get("Retry-After", "5")
try:
wait_seconds = float(retry_after)
except ValueError:
wait_seconds = 5.0
logger.warning(
"TickDB rate limit reached",
extra={
"code": code,
"retry_after": wait_seconds,
"attempt": attempt + 1,
},
)
time.sleep(max(wait_seconds, 0.1))
continue
try:
response.raise_for_status()
except requests.HTTPError as exc:
raise RuntimeError(
f"TickDB HTTP failure: {response.status_code}"
) from exc
if code == 0:
return payload.get("data")
if code in (1001, 1002):
raise TickDBAPIError(
code, "Invalid or missing API key; check TICKDB_API_KEY"
)
if code == 2002:
raise TickDBAPIError(
code, "Symbol not found; verify it through /v1/symbols/available"
)
raise TickDBAPIError(code, payload.get("message", "Unknown error"))
raise RuntimeError("TickDB rate limit persisted after retries")
def fetch_completed_bars(symbol, interval, limit=500):
"""Historical OHLCV for completed periods."""
return tickdb_get(
"/v1/market/kline",
{
"symbol": symbol,
"interval": interval,
"limit": limit,
},
)
def fetch_latest_bar(symbol, interval="1m"):
"""Current real-time candle; do not use as historical backtest data."""
return tickdb_get(
"/v1/market/kline/latest",
{
"symbol": symbol,
"interval": interval,
},
)
def add_query_token(url, token):
"""Put the private-stream credential in the URL query parameter."""
parsed = urlparse(url)
query = dict(parse_qsl(parsed.query))
query["token"] = token
return urlunparse(parsed._replace(query=urlencode(query)))
def normalize_execution_event(payload):
"""Map a broker-specific event into the normalized audit schema."""
if payload.get("type") not in {"execution", "fill"}:
return None
return {
"type": "fill",
"sequence": int(payload["sequence"]),
"symbol": str(payload["symbol"]),
"side": str(payload["side"]).lower(),
"quantity": float(payload["quantity"]),
"fill_price": float(payload["fill_price"]),
"reference_price": float(payload["reference_price"]),
"event_timestamp": str(payload["event_timestamp"]),
"client_order_id": str(payload.get("client_order_id", "")),
}
def parse_timestamp(value):
normalized = value.replace("Z", "+00:00")
parsed = datetime.fromisoformat(normalized)
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed.timestamp()
def calculate_slippage_bps(event):
"""Positive values indicate adverse slippage."""
reference = event["reference_price"]
fill = event["fill_price"]
if reference <= 0:
raise ValueError("reference_price must be positive")
if event["side"] == "buy":
return (fill - reference) / reference * 10_000
if event["side"] == "sell":
return (reference - fill) / reference * 10_000
raise ValueError(f"Unsupported side: {event['side']}")
def write_audit_record(record):
with AUDIT_PATH.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record, sort_keys=True) + "\n")
async def heartbeat(ws):
"""Application-level heartbeat in addition to transport-level pings."""
while True:
await asyncio.sleep(15)
await ws.send(json.dumps({"cmd": "ping"}))
async def consume_execution_stream():
"""Consume fills with reconnect, backoff, jitter, and gap detection."""
stream_url = add_query_token(
PRIVATE_STREAM_URL,
PRIVATE_STREAM_TOKEN,
)
attempt = 0
previous_sequence = None
reconciliation_required = False
while True:
heartbeat_task = None
try:
async with websockets.connect(
stream_url,
open_timeout=5,
ping_interval=20,
ping_timeout=10,
close_timeout=5,
) as ws:
attempt = 0
logger.info("Private execution stream connected")
await ws.send(
json.dumps(
{
"cmd": "subscribe",
"channel": "executions",
}
)
)
heartbeat_task = asyncio.create_task(heartbeat(ws))
while True:
raw = await asyncio.wait_for(ws.recv(), timeout=35)
payload = json.loads(raw)
if payload.get("type") == "pong":
continue
event = normalize_execution_event(payload)
if event is None:
continue
sequence = event["sequence"]
if (
previous_sequence is not None
and sequence > previous_sequence + 1
):
reconciliation_required = True
logger.error(
"Execution sequence gap detected",
extra={
"previous_sequence": previous_sequence,
"received_sequence": sequence,
"missing_count": sequence
- previous_sequence
- 1,
},
)
previous_sequence = sequence
received_at = time.time()
event_timestamp = parse_timestamp(
event["event_timestamp"]
)
latency_ms = max(
0.0,
(received_at - event_timestamp) * 1_000,
)
slippage_bps = calculate_slippage_bps(event)
record = {
**event,
"received_timestamp": datetime.now(
timezone.utc
).isoformat(),
"latency_ms": round(latency_ms, 3),
"slippage_bps": round(slippage_bps, 4),
"reconciliation_required": reconciliation_required,
}
write_audit_record(record)
if slippage_bps > MAX_SLIPPAGE_BPS:
logger.warning(
"Slippage tolerance exceeded",
extra={
"symbol": event["symbol"],
"slippage_bps": round(slippage_bps, 4),
"limit_bps": MAX_SLIPPAGE_BPS,
},
)
if latency_ms > MAX_LATENCY_MS:
logger.warning(
"Latency tolerance exceeded",
extra={
"symbol": event["symbol"],
"latency_ms": round(latency_ms, 3),
"limit_ms": MAX_LATENCY_MS,
},
)
if reconciliation_required:
logger.error(
"New orders disabled until broker reconciliation",
extra={"client_order_id": event["client_order_id"]},
)
reconciliation_required = False
except (
asyncio.TimeoutError,
websockets.ConnectionClosed,
TimeoutError,
ConnectionError,
) as exc:
logger.warning(
"Execution stream disconnected",
extra={"error": str(exc)},
)
except Exception:
logger.exception("Unexpected execution-stream failure")
finally:
if heartbeat_task is not None:
heartbeat_task.cancel()
await asyncio.gather(
heartbeat_task,
return_exceptions=True,
)
delay = min(1.0 * (2 ** attempt), 30.0)
jitter = random.uniform(0, delay * 0.1)
attempt += 1
logger.info(
"Reconnecting",
extra={"delay_seconds": round(delay + jitter, 3)},
)
await asyncio.sleep(delay + jitter)
async def main():
# Verify API access and preserve the raw provider schema for research.
completed = fetch_completed_bars(
TICKDB_SYMBOL,
TICKDB_INTERVAL,
limit=100,
)
latest = fetch_latest_bar(TICKDB_SYMBOL, TICKDB_INTERVAL)
logger.info(
"Market-data endpoints initialized",
extra={
"symbol": TICKDB_SYMBOL,
"completed_payload_type": type(completed).__name__,
"latest_payload_type": type(latest).__name__,
},
)
await consume_execution_stream()
if __name__ == "__main__":
asyncio.run(main())
Install the required packages and configure the environment before running the monitor:
pip install requests websockets
export TICKDB_API_KEY="your-api-key"
export TICKDB_SYMBOL="AAPL.US"
export TICKDB_INTERVAL="1m"
export PRIVATE_STREAM_URL="wss://your-private-stream.example/v1/executions"
export PRIVATE_STREAM_TOKEN="your-private-stream-token"
export MAX_SLIPPAGE_BPS="10"
export MAX_LATENCY_MS="250"
5.1 Production warnings
The implementation includes several deliberate engineering boundaries:
- Every HTTP request has a connect and read timeout.
- The API key comes from an environment variable.
- TickDB rate-limit code
3001triggersRetry-Afterhandling. - The WebSocket has application-level and transport-level heartbeats.
- Reconnection uses exponential backoff and random jitter.
- Sequence gaps trigger reconciliation.
- Audit records survive process restarts because they are appended to disk.
- The code does not assume that a new submission is safe after an ambiguous disconnect.
For high-frequency workloads, replace synchronous requests calls with aiohttp or asyncio, and measure queueing delay inside the process. Network latency is only one component of end-to-end delay.
6. Gap Four: Overfitting and the Validation Gap
Production engineering cannot repair a strategy whose apparent edge came from data mining. Overfitting often enters through the backtest workflow itself.
Common sources include:
- Trying many parameter combinations and retaining the best result.
- Selecting an asset or period after observing performance.
- Using future-adjusted information unavailable at decision time.
- Treating overlapping trades as independent observations.
- Ignoring fees, spread, slippage, borrow costs, or market impact.
- Comparing the strategy with a benchmark selected after the fact.
- Revising rules while reviewing live trades.
6.1 Separate four evaluation layers
A robust evaluation process uses four distinct datasets or periods:
| Layer | Purpose | Permitted activity |
|---|---|---|
| Development | Implement and debug the rules | Inspect failures and revise logic |
| Validation | Select model and parameters | Compare a limited set of candidates |
| Test | Estimate final performance | Evaluate once; do not tune against it |
| Live shadow or paper | Validate infrastructure and execution | Test order state, latency, and alerts |
Do not repeatedly inspect the test set. Every revision converts it into a development set and creates another layer of selection bias.
6.2 Evaluate tradable outcomes, not ideal orders
A backtest should count:
- Actual position size.
- Partial fills and missed fills.
- Spread crossing.
- Venue and routing costs.
- Commission and fee schedules.
- Borrow availability where relevant.
- Delay between signal and order.
- Strategy behavior when a position cannot be opened.
- Forced reduction after a risk limit.
An unrealistically precise fill model can turn a weak signal into a profitable chart. Realism often makes a strategy look worse before it makes it trustworthy.
6.3 Disclose the evidence before deployment
This article does not report strategy returns. Before deploying a live strategy, document at least the following:
| Metric | Required disclosure |
|---|---|
| Backtest period | Exact start and end dates |
| Live or paper period | Exact start and end dates |
| Sample size | Number of trades and independent events |
| Win rate | Gross and net of costs |
| Profit factor | Gross and net of costs |
| Risk-adjusted return | Sharpe and preferably Sortino |
| Maximum drawdown | Magnitude and duration |
| Benchmark | Relevant index and buy-and-hold comparison |
| Cost assumptions | Spread, slippage, commission, impact |
| Regime coverage | Bull, bear, high-volatility, low-liquidity periods |
Backtest limitations: Historical simulation does not guarantee future performance. Slippage and market impact are approximations; the model may not capture liquidity exhaustion during extreme events; limited samples can reduce statistical significance; and live latency, partial fills, rejects, or disconnects can change the result. Validate the strategy out of sample and in shadow execution before risking capital.
7. Gap Five: Mindset and Execution Consistency
The final gap cannot be eliminated through a better model alone. It appears when the operator changes a tested strategy because the live result feels wrong.
Common live-trading interventions include:
- Disabling a trade after several consecutive losses.
- Increasing size after a win.
- Manually closing a position before the strategy exit.
- Adding a filter visible only in live execution.
- Re-entering after the original exit condition disappears.
- Changing parameters during a drawdown.
Each intervention changes the strategy. A rule-modified system is no longer the strategy represented by the backtest.
7.1 Make operational rules explicit
An operator needs a written policy for:
| Situation | Required behavior |
|---|---|
| Missing market data | Enter a defined safe state |
| Broker rejection | Retry, cancel, or escalate according to order type |
| Uncertain order state | Reconcile before submitting another order |
| Excessive slippage | Pause entry or reduce participation rate |
| Latency breach | Disable the affected signal or use a slower execution path |
| Risk-limit breach | Flatten, hedge, or halt according to a predefined policy |
| Strategy drawdown | Follow a predeclared review procedure |
| Software defect | Disable affected signals and preserve evidence |
The key distinction is between risk controls and strategy discretion. A kill switch can be independent of the strategy. Manual entry selection usually cannot.
7.2 Reduce the need for emotional override
Live behavior improves when the system removes opportunities for improvisation:
- Provide a clear dashboard showing data age, order state, exposure, and connection health.
- Display whether the current trading mode is live, shadow, or halted.
- Require an audit record for every manual action.
- Make strategy parameters immutable during the live session unless a controlled deployment is required.
- Show the current drawdown against a predefined escalation threshold.
- Use checklists for incident response.
- Separate development and production credentials.
- Restrict production changes through versioned configuration.
The goal is not to remove human judgment. It is to prevent unplanned judgment from masquerading as the strategy being tested.
8. Use a Capability-Aware Data Contract
A parity program must use data whose semantics match the decision it supports.
TickDB’s data boundaries should shape the research design:
| Data need | Appropriate use | Boundary |
|---|---|---|
| Completed historical OHLCV | Strategy research and backtesting | Use /v1/market/kline for completed periods |
| Current real-time candle | Live monitoring | Use /v1/market/kline/latest |
| US equity order-book depth | L1 snapshot analysis | US depth support is limited to L1 |
| HK or crypto order-book depth | Multi-level liquidity analysis | HK and crypto support L1–L10 where available |
| HK or crypto trades | Tick-level order-flow analysis | US equity and A-share trades are not supported |
| Forex, precious-metal, or index depth | Not supported | Do not design the strategy around unavailable levels |
This boundary matters because a slippage model based on five visible book levels cannot be transferred to a market where only one level is available. Likewise, a US-equity backtest cannot assume tick-level trade data simply because the strategy logic resembles a crypto strategy.
A reliable data contract should record:
- Symbol and venue.
- Bar interval and timezone.
- Timestamp semantics.
- Whether values are revised.
- Corporate-action treatment.
- Missing-data policy.
- Whether the observation was available at decision time.
- Any difference between research and live symbol identifiers.
9. Compare the Same Decision Path
The most useful backtest-versus-live report compares decisions rather than only returns.
| Layer | Backtest | Live | Required comparison |
|---|---|---|---|
| Signal input | Historical series at timestamp (t) | Real-time series at timestamp (t) | Value, freshness, missing fields |
| Decision | Deterministic rule | Same rule plus validated overrides | Signal value and reason |
| Execution | Modeled fill | Broker/venue fill | Expected versus realized price |
| Latency | Usually fixed or ignored | Measured by stage | Decision-to-ack and event-to-receive |
| Connectivity | Assumed available | Reconnect and reconciliation | Gap count and recovery duration |
| Risk state | Internal counters | Broker and internal state | Position and exposure difference |
| Costs | Assumed schedule | Actual fees and impact | Cost per trade |
| Behavior | 100% rule adherence | Recorded manual interventions | Deviation count and reason |
Return-level monitoring alone identifies failure too late. Execution-level telemetry identifies the cause while the system is still small and diagnosable.
A useful daily report should include:
- Fill count and fill rate.
- Mean, median, and 95th-percentile slippage.
- Decision-to-ack and event-to-receive latency.
- Reconnect count and total disconnected duration.
- Event-sequence gaps and reconciliation duration.
- Order rejects and duplicate-order prevention events.
- Position mismatches between internal and broker state.
- Live-versus-modeled cost per trade.
- Manual override count.
- Strategy metrics calculated with the same code as the backtest.
10. Deploy Through Controlled Stages
A gradual deployment reduces the blast radius of unresolved assumptions.
| Stage | Objective | Entry criteria | Exit criteria |
|---|---|---|---|
| Local replay | Validate decision logic | Deterministic fixtures | Replays match expected decisions |
| Shadow mode | Generate orders without sending them | Live data connected | Parity errors remain within tolerance |
| Broker sandbox | Test order state and rejects | Reconciliation works | No duplicate or orphaned orders |
| Micro-capital | Measure real execution | Risk limits active | Slippage and latency remain acceptable |
| Scaled deployment | Increase participation | Live evidence supports scaling | Exposure and drawdown remain within limits |
10.1 Individual developer
An individual developer should begin with shadow mode, explicit order-state logging, and a small set of symbols. The priority is operational visibility rather than maximum strategy complexity.
10.2 Quant team
A team should add centralized audit logs, deployment versioning, broker-state reconciliation, and independent risk controls. Production credentials and research environments should remain separated.
10.3 Institutional deployment
An institutional deployment should add:
- Broker and venue diversification.
- Centralized order management.
- Redundant market-data paths.
- Clock-synchronization monitoring.
- Immutable execution records.
- Capacity and participation controls.
- Formal model-governance reviews.
- Independent reconciliation between internal and external state.
The deployment limit should be determined by observed execution quality, not by the strategy’s historical return.
11. A Practical Acceptance Checklist
Before moving a strategy from backtest to live, require evidence for every question below.
Data and research
- The backtest uses completed-period data.
- No future information enters the signal.
- Costs reflect observed or conservatively estimated slippage.
- The test set has not been repeatedly optimized against.
- Live and backtest calculations use the same position logic.
- Results disclose period, sample size, drawdown, and benchmark.
Execution
- Expected and realized prices use the same reference definition.
- Fill rate and partial-fill behavior are modeled.
- Event and order timestamps use synchronized clocks.
- Stale data is rejected according to a documented threshold.
- Signal half-life exceeds the expected end-to-end latency.
Reliability
- Reconnection uses exponential backoff and jitter.
- Ambiguous submissions enter an
UNKNOWNstate. - Sequence gaps trigger reconciliation.
- Duplicate orders are prevented through client order IDs.
- Broker and internal positions are reconciled after recovery.
- New entries remain disabled until state is valid.
Human operations
- Every manual intervention has a reason and audit record.
- Strategy parameters cannot drift silently.
- Risk limits and kill switches are independent of discretionary overrides.
- The operator dashboard displays data age, exposure, and connection state.
- A live loss does not automatically change a tested rule.
Conclusion
At 09:30:01, the backtest places a buy order at 101.25, while the live system receives a fill at 101.34.
The difference is not mysterious. It is measurable.
Closing the five gaps requires treating slippage, latency, disconnection, overfitting, and execution consistency as separate failure domains. Instrument the decision path, model realistic fills, recover from uncertain state, validate the strategy out of sample, and prevent unplanned operator changes from rewriting the rules.
TickDB can support this workflow through completed-period OHLCV from /v1/market/kline, current candles from /v1/market/kline/latest, and depth or trade data where the asset class supports it. Its capability boundaries must still shape the strategy: US equity tick trades are not available, US depth is limited to L1, and depth is unavailable for forex, precious metals, and indices.
Next Steps
If you are an investor looking for deeper market insight, subscribe to the TickDB newsletter for weekly supply-chain and microstructure analysis.
If you want to reproduce the parity monitoring workflow:
- Sign up at tickdb.ai for a free API key.
- Generate the key in the dashboard.
- Set
TICKDB_API_KEYin your environment. - Connect the normalized private execution stream.
- Compare modeled fills with realized fills before increasing capital.
If you need 10+ years of historical OHLCV data for institutional backtesting, contact [email protected] for Professional or Enterprise plans.
If you use AI coding assistants, search for and install the tickdb-market-data SKILL in your AI tool’s marketplace.
This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results.