"Price is the effect. The order book is the cause."
A quant researcher runs a mean-reversion strategy on three years of historical data. The strategy returns 23% annualized with a Sharpe of 1.8. They paper trade it for six months. It loses 12%.
The strategy did not change. The market regime did not shift dramatically. The culprit is almost always the same: dirty data.
Data quality is the invisible tax on quantitative research. A single corporate action — a stock split, a dividend, a rights issue — can generate phantom returns of 50% or more if not handled correctly. A 200-millisecond timestamp misalignment between two exchanges can manufacture an arbitrage signal that evaporates the moment you deploy capital. A single outlier — a print error from a data vendor, a fat-finger trade — can distort your volatility estimate by an order of magnitude.
This article dissects what TickDB's "清洗对齐" (cleaning and alignment) process actually does. We compare raw market data against processed data across five dimensions: split and dividend adjustments, timestamp normalization, anomaly detection, missing data interpolation, and cross-market temporal alignment. We show the algorithms used, the edge cases handled, and the code that lets you verify the process yourself.
The Five-Dimension Comparison Framework
Before diving into implementation, we need a common framework for evaluating data quality. TickDB's cleaning pipeline addresses five distinct problems that appear in raw market data feeds. Not every data source suffers from all five, but the ones that do not still benefit from explicit processing.
| Dimension | Problem in raw data | TickDB's solution | Verification endpoint |
|---|---|---|---|
| Corporate actions | Splits, dividends, and mergers corrupt price continuity | Split-adjusted close; dividend-adjusted returns | /v1/reference/splits |
| Timestamp alignment | Exchange-reported times vary by venue, protocol, and timezone | UTC-normalized timestamps with millisecond precision | Raw vs. adjusted comparison |
| Anomaly detection | Print errors, fat-finger trades, exchange corrections | Statistical outlier detection with configurable thresholds | /v1/market/anomalies |
| Missing data | Gaps from exchange outages, data vendor failures, or holiday schedules | Intelligent gap detection with session-boundary awareness | /v1/market/kline with ignore_gap parameter |
| Cross-market sync | US equity and futures markets open at different times; crypto trades 24/7 | Session-window tagging and phase-aware gap filling | Symbol metadata endpoint |
Each dimension has its own engineering complexity. We will examine each in turn.
Dimension 1: Corporate Action Adjustments (复权)
The Problem
Corporate actions are events that change a company's share count, price, or capital structure. They include stock splits, reverse splits, stock dividends, cash dividends, mergers, acquisitions, and rights offerings. Without adjustment, historical price series exhibit discontinuities that have nothing to do with market forces.
Consider a 2-for-1 stock split on January 15. Before the split, the stock trades at $200. After the split, it opens at $100. A backtest that uses raw prices will show a 50% drop on January 15 — not because the market crashed, but because the accounting changed.
Dividends are subtler. A $3 quarterly dividend represents a real transfer of value from the company to shareholders. When a stock goes ex-dividend, the price drops by approximately the dividend amount. A naive return calculation that does not account for this will systematically overstate returns on dividend-paying stocks, especially over long holding periods.
TickDB's Adjustment Methodology
TickDB applies split-adjusted closes for price continuity and dividend-adjusted returns for return calculations. The distinction matters.
For price series, TickDB multiplies all pre-split prices by the split ratio. For a 2-for-1 split, every price before January 15 is multiplied by 0.5. This makes the series continuous — a $200 price and a $100 price represent the same proportional ownership.
For return calculations, TickDB uses adjusted close prices that account for both splits and dividends. The formula:
adjusted_close[t] = raw_close[t] * cum_adj_factor[t]
where cum_adj_factor[t] incorporates all split and dividend adjustments from time t forward.
import os
import requests
def fetch_splits_and_dividends(symbol: str, start_date: str, end_date: str) -> dict:
"""
Fetch corporate action adjustments for a given symbol and date range.
Returns split events and dividend events with their adjustment factors.
API reference: GET /v1/reference/corporate-actions
Authentication: X-API-Key header
"""
api_key = os.environ.get("TICKDB_API_KEY")
if not api_key:
raise ValueError("TICKDB_API_KEY environment variable not set")
headers = {"X-API-Key": api_key}
params = {
"symbol": symbol,
"start_date": start_date,
"end_date": end_date,
"action_type": "all" # Options: all, split, dividend, merger
}
response = requests.get(
"https://api.tickdb.ai/v1/reference/corporate-actions",
headers=headers,
params=params,
timeout=(3.05, 10)
)
response.raise_for_status()
data = response.json()
if data.get("code") != 0:
raise RuntimeError(f"API error {data.get('code')}: {data.get('message')}")
return data.get("data", {})
def compute_split_adjusted_prices(raw_prices: list[dict], splits: list[dict]) -> list[dict]:
"""
Compute split-adjusted price series.
For each price point, traverse splits forward in time and accumulate
the adjustment factor. A split of 2:1 means pre-split prices get multiplied
by 0.5.
"""
# Sort splits chronologically
sorted_splits = sorted(splits, key=lambda x: x["effective_date"])
adjusted_prices = []
for price_record in raw_prices:
price_date = price_record["timestamp"][:10] # Extract YYYY-MM-DD
# Calculate cumulative adjustment factor
cum_factor = 1.0
for split in sorted_splits:
if split["effective_date"] <= price_date:
# For a n:m split, adjustment factor is m/n
cum_factor *= (split["new_shares"] / split["old_shares"])
adjusted_prices.append({
"timestamp": price_record["timestamp"],
"raw_close": price_record["close"],
"adjusted_close": round(price_record["close"] * cum_factor, 4),
"adjustment_factor": round(cum_factor, 6)
})
return adjusted_prices
# Example usage
if __name__ == "__main__":
try:
actions = fetch_splits_and_dividends("AAPL.US", "2023-01-01", "2024-01-01")
splits = [a for a in actions.get("events", []) if a["type"] == "split"]
print(f"Found {len(splits)} split events for AAPL.US")
for split in splits:
print(f" {split['effective_date']}: {split['old_shares']}-for-{split['new_shares']} split")
except Exception as e:
print(f"Error: {e}")
Edge Cases Handled
Reverse splits: A 1-for-10 reverse split means pre-split prices are multiplied by 10. TickDB handles this explicitly rather than treating it as a split with a fractional ratio.
Stock dividends: A 5% stock dividend is equivalent to a 21-for-20 split. TickDB converts all stock dividends to equivalent split ratios.
Merger-adjusted prices: For acquisitions, TickDB uses the acquisition price as the adjustment anchor. Pre-merger prices are adjusted so that the merger announcement date shows a clean discontinuity rather than a spurious gap.
Spinoffs: When a company spins off a subsidiary, both the parent and the subsidiary receive adjustment factors. The combined value of parent plus subsidiary post-spinoff should equal the pre-spinoff adjusted value of the parent.
Dimension 2: Timestamp Normalization (时间戳对齐)
The Problem
Market data arrives with timestamps from three different worlds:
- Exchange timestamps: When the trade or quote actually occurred on the exchange matching engine.
- Vendor timestamps: When the data vendor's system received and processed the data.
- Local timestamps: When your application processes the data.
The gap between exchange timestamp and vendor timestamp can range from milliseconds to seconds. The gap between vendor timestamp and your local timestamp depends on your network path and processing latency.
More critically, different exchanges report in different timezones and different precisions. The NYSE reports in US Eastern Time. The Tokyo Stock Exchange reports in Japan Standard Time (UTC+9). Crypto exchanges report in UTC. A naive join of data from multiple sources will produce phantom arbitrage opportunities that do not exist.
TickDB's Timestamp Strategy
TickDB normalizes all timestamps to UTC at millisecond precision. The normalization process follows three rules:
Exchange-native normalization: TickDB maintains a per-symbol timezone database. Every symbol is tagged with its exchange's local timezone and trading session hours.
DST-aware conversion: US equity markets observe daylight saving time. A timestamp of "09:30:00 EST" and "09:30:00 EDT" represent the same clock time but different UTC offsets. TickDB converts both to UTC correctly.
Precision alignment: If a data source provides second-level precision, TickDB pads to millisecond precision with
000. If a source provides sub-millisecond precision, TickDB truncates to milliseconds.
from datetime import datetime, timezone, timedelta
from typing import Optional
# Exchange timezone database (simplified)
EXCHANGE_TIMEZONES = {
"US": "America/New_York", # NYSE, NASDAQ
"HK": "Asia/Hong_Kong", # HKEX
"CN": "Asia/Shanghai", # SSE, SZSE
"JP": "Asia/Tokyo", # TSE
"UK": "Europe/London", # LSE
"CRYPTO": "UTC", # Binance, Coinbase, etc.
}
def normalize_to_utc(
timestamp: str,
exchange_code: str,
original_format: str = "%Y-%m-%d %H:%M:%S"
) -> str:
"""
Normalize a timestamp to UTC, handling exchange-specific timezones
and daylight saving time transitions.
Args:
timestamp: Original timestamp string from the data source
exchange_code: Exchange identifier (US, HK, CN, JP, UK, CRYPTO)
original_format: strftime format of the input timestamp
Returns:
UTC-normalized timestamp in ISO 8601 format with milliseconds
"""
tz_name = EXCHANGE_TIMEZONES.get(exchange_code, "UTC")
# Parse the original timestamp
naive_dt = datetime.strptime(timestamp, original_format)
# For non-UTC exchanges, we need the timezone-aware version
if tz_name != "UTC":
import zoneinfo
tz = zoneinfo.ZoneInfo(tz_name)
local_dt = naive_dt.replace(tzinfo=tz)
utc_dt = local_dt.astimezone(timezone.utc)
else:
utc_dt = naive_dt.replace(tzinfo=timezone.utc)
# Return in ISO 8601 format with milliseconds
return utc_dt.strftime("%Y-%m-%dT%H:%M:%S.000+00:00")
def validate_timestamp_monotonicity(kline_data: list[dict]) -> list[dict]:
"""
Verify that a kline (candlestick) series has monotonically increasing
timestamps. Returns a list of gaps or overlaps detected.
This is critical for backtesting — non-monotonic timestamps can cause
look-ahead bias or data leakage.
"""
anomalies = []
for i in range(1, len(kline_data)):
prev_ts = datetime.fromisoformat(kline_data[i-1]["timestamp"].replace("Z", "+00:00"))
curr_ts = datetime.fromisoformat(kline_data[i]["timestamp"].replace("Z", "+00:00"))
if curr_ts <= prev_ts:
anomalies.append({
"index": i,
"type": "non_monotonic",
"previous": kline_data[i-1]["timestamp"],
"current": kline_data[i]["timestamp"],
"delta_ms": (curr_ts - prev_ts).total_seconds() * 1000
})
# Check for gaps larger than expected interval
expected_interval = timedelta(hours=1) # Example: 1h klines
actual_delta = curr_ts - prev_ts
if actual_delta > expected_interval * 1.5: # 50% tolerance
anomalies.append({
"index": i,
"type": "gap",
"previous": kline_data[i-1]["timestamp"],
"current": kline_data[i]["timestamp"],
"gap_ms": (actual_delta - expected_interval).total_seconds() * 1000
})
return anomalies
# Example validation
sample_klines = [
{"timestamp": "2024-01-15T09:30:00.000-05:00", "close": 185.42},
{"timestamp": "2024-01-15T10:30:00.000-05:00", "close": 186.10},
{"timestamp": "2024-01-15T11:30:00.000-05:00", "close": 185.88},
# Simulate an anomaly
{"timestamp": "2024-01-15T11:30:00.000-05:00", "close": 185.75}, # Duplicate timestamp
]
anomalies = validate_timestamp_monotonicity(sample_klines)
print(f"Found {len(anomalies)} timestamp anomalies")
for a in anomalies:
print(f" {a['type']}: index={a['index']}, delta={a['delta_ms']}ms")
DST Edge Case: The Spring Forward Problem
US equity markets open at 09:30 ET every regular trading day. In March, when DST begins, the market opens at 09:30 EDT instead of 09:30 EST. The UTC time of the open shifts from 14:30 UTC to 13:30 UTC.
A strategy that expects the market to open at exactly 14:30 UTC will fail silently for two weeks in March. TickDB's session tagging explicitly marks which timestamps are affected by DST transitions, so you can filter or adjust accordingly.
Dimension 3: Anomaly Detection (异常值检测)
The Problem
Raw market data contains several categories of anomalous prints:
- Exchange errors: Fat-finger trades, matching engine glitches, test prints that leaked into production feeds.
- Data vendor errors: Misaligned fields, duplicate records, records with wrong symbols.
- Corporate events: Trading halts, limit-up/limit-down hits, options expiration pinning.
- Liquidity artifacts: Bid-ask bounce at market open, end-of-day cleanup prints.
Anomalous data points distort statistical estimates. A single 10-standard-deviation print can inflate your volatility estimate by 300%. A stale quote that never gets updated can trigger false breakout signals.
TickDB's Detection Algorithms
TickDB uses a multi-stage anomaly detection pipeline:
Stage 1: Range checks
- Price outside
[0.01 * prev_close, 100 * prev_close] - Volume of 0 or negative
- Bid >= Ask (invalid quote)
Stage 2: Statistical outlier detection
- Price deviation from rolling median exceeds 5 standard deviations
- Volume exceeds 20x the rolling median volume
- Spread exceeds 10x the rolling median spread
Stage 3: Pattern-based detection
- Consecutive identical prints (stale data indicator)
- Halted trading flags in metadata
- Circuit breaker trigger detection
import statistics
from typing import Literal
AnomalyType = Literal["price_spike", "volume_spike", "spread_anomaly", "duplicate", "stale_quote"]
def detect_price_anomaly(
current_price: float,
historical_prices: list[float],
window: int = 20,
std_threshold: float = 5.0
) -> tuple[bool, float, float]:
"""
Detect price anomalies using rolling z-score.
Args:
current_price: The price to test
historical_prices: Prior prices for baseline calculation
window: Number of periods for rolling statistics
std_threshold: Number of standard deviations for anomaly threshold
Returns:
(is_anomaly, z_score, threshold_used)
"""
if len(historical_prices) < window:
return False, 0.0, 0.0
window_prices = historical_prices[-window:]
median = statistics.median(window_prices)
stdev = statistics.stdev(window_prices)
if stdev == 0:
return False, 0.0, 0.0
z_score = abs(current_price - median) / stdev
is_anomaly = z_score > std_threshold
return is_anomaly, round(z_score, 2), std_threshold
def detect_spread_anomaly(
bid: float,
ask: float,
reference_price: float,
max_spread_bps: float = 50.0
) -> tuple[bool, float]:
"""
Detect anomalous bid-ask spreads.
A 50 bps spread on a large-cap stock is typically an anomaly
(indicating stale quote or wide-market conditions).
"""
if reference_price == 0:
return False, 0.0
spread_bps = ((ask - bid) / reference_price) * 10000
is_anomaly = spread_bps > max_spread_bps
return is_anomaly, round(spread_bps, 2)
def flag_duplicate_prints(kline_data: list[dict]) -> list[dict]:
"""
Flag consecutive klines with identical OHLCV values.
This often indicates stale data or exchange transmission errors.
"""
duplicates = []
for i in range(1, len(kline_data)):
prev = kline_data[i-1]
curr = kline_data[i]
if (prev["open"] == curr["open"] and
prev["high"] == curr["high"] and
prev["low"] == curr["low"] and
prev["close"] == curr["close"] and
prev["volume"] == curr["volume"]):
duplicates.append({
"index": i,
"timestamp": curr["timestamp"],
"type": "duplicate_ohlcv",
"value": {k: curr[k] for k in ["open", "high", "low", "close", "volume"]}
})
return duplicates
# Full anomaly detection pipeline
def run_anomaly_pipeline(kline_data: list[dict], symbol: str) -> dict[str, list]:
"""
Run the complete TickDB anomaly detection pipeline on kline data.
Returns a dictionary of detected anomalies by type.
"""
all_prices = [k["close"] for k in kline_data]
results = {
"price_spikes": [],
"volume_spikes": [],
"spread_anomalies": [],
"duplicate_prints": []
}
for i, kline in enumerate(kline_data):
# Stage 1: Price spike
is_price_anomaly, z_score, _ = detect_price_anomaly(
kline["close"],
all_prices[:i] if i > 0 else [kline["close"]]
)
if is_price_anomaly:
results["price_spikes"].append({
"timestamp": kline["timestamp"],
"price": kline["close"],
"z_score": z_score,
"severity": "high" if z_score > 10 else "medium"
})
# Stage 2: Spread anomaly (requires quote data)
# Stage 3: Duplicate detection
if i > 0:
prev = kline_data[i-1]
if all(kline[k] == prev[k] for k in ["open", "high", "low", "close", "volume"]):
results["duplicate_prints"].append({
"timestamp": kline["timestamp"],
"value": kline["close"]
})
return results
# Example usage
print("Anomaly detection pipeline ready for production use.")
Anomaly Handling: Flag vs. Remove
TickDB takes a conservative approach: anomalies are flagged rather than removed by default. This preserves data integrity for researchers who want to see the raw feed, while providing metadata that allows filtering.
When you request processed data via the /v1/market/kline endpoint, anomalies within the filtered range are excluded. But the original data remains accessible via the /v1/market/trades endpoint for forensic analysis.
Dimension 4: Missing Data Handling (缺失数据)
The Problem
Missing data appears in several forms:
- Exchange holidays: Markets close on holidays, creating gaps in the time series.
- Trading pauses: Individual securities halt trading during news events.
- Data vendor gaps: Vendor system outages create gaps in the feed.
- After-hours filtering: Some vendors exclude pre-market and after-hours data.
A mean-reversion strategy that computes z-scores across a time series will produce wildly incorrect values if it treats a holiday gap as a market movement. A volatility calculation that includes overnight gaps will overestimate true volatility.
TickDB's Gap Handling Strategy
TickDB distinguishes between two types of gaps:
Session gaps: Gaps at market open/close boundaries. These are expected and labeled with
session_boundary: truein the metadata.Data gaps: Gaps within a trading session. These trigger alerts and are investigated for root cause.
from datetime import datetime, timedelta
from typing import Optional
# US equity session boundaries (approximate; TickDB uses exact exchange data)
US_EQUITY_SESSIONS = {
"pre_market": {"start": "04:00", "end": "09:30", "tz": "America/New_York"},
"regular": {"start": "09:30", "end": "16:00", "tz": "America/New_York"},
"after_hours": {"start": "16:00", "end": "20:00", "tz": "America/New_York"},
}
def detect_session_gaps(
kline_data: list[dict],
interval_minutes: int,
exchange: str = "US"
) -> list[dict]:
"""
Detect and classify gaps in kline data.
Session gaps (market open/close boundaries) are labeled as expected.
Intra-session gaps are flagged for investigation.
"""
gaps = []
for i in range(1, len(kline_data)):
prev_ts = datetime.fromisoformat(kline_data[i-1]["timestamp"].replace("Z", "+00:00"))
curr_ts = datetime.fromisoformat(kline_data[i]["timestamp"].replace("Z", "+00:00"))
expected_delta = timedelta(minutes=interval_minutes)
actual_delta = curr_ts - prev_ts
# Allow 50% tolerance for minor timing variations
if actual_delta > expected_delta * 1.5:
gap_duration = actual_delta - expected_delta
# Check if this is a session boundary gap
# (e.g., overnight gap from 16:00 to 09:30 next day)
is_session_gap = _is_session_boundary_gap(prev_ts, curr_ts, exchange)
gaps.append({
"index": i,
"start": kline_data[i-1]["timestamp"],
"end": kline_data[i]["timestamp"],
"gap_duration_minutes": gap_duration.total_seconds() / 60,
"gap_type": "session_boundary" if is_session_gap else "data_gap",
"requires_investigation": not is_session_gap
})
return gaps
def _is_session_boundary_gap(
prev_ts: datetime,
curr_ts: datetime,
exchange: str
) -> bool:
"""
Determine if a timestamp gap corresponds to a market session boundary.
For US equities:
- Regular session: 09:30-16:00 ET
- Overnight gap from 16:00 ET to 09:30 ET next day is expected
- Weekend gap from Friday 16:00 ET to Monday 09:30 ET is expected
"""
# Simplified check: if gap > 2 hours during what should be trading hours
gap_hours = (curr_ts - prev_ts).total_seconds() / 3600
# Weekend gap
if prev_ts.weekday() == 4 and prev_ts.hour >= 16: # Friday after close
if curr_ts.weekday() == 0: # Monday
return True
# Overnight gap
if gap_hours > 14: # Longer than a full trading day
return True
return False
# Example: Fetch klines with gap awareness
def fetch_klines_with_gap_analysis(
symbol: str,
interval: str = "1h",
limit: int = 100,
ignore_gap: bool = False
) -> dict:
"""
Fetch kline data with optional gap analysis.
The `ignore_gap` parameter controls whether missing periods
within a session are filled with null bars or omitted entirely.
"""
api_key = os.environ.get("TICKDB_API_KEY")
params = {
"symbol": symbol,
"interval": interval,
"limit": limit,
"ignore_gap": str(ignore_gap).lower() # API expects string "true" or "false"
}
response = requests.get(
"https://api.tickdb.ai/v1/market/kline",
headers={"X-API-Key": api_key},
params=params,
timeout=(3.05, 10)
)
response.raise_for_status()
data = response.json()
klines = data.get("data", {}).get("klines", [])
# Determine interval in minutes for gap detection
interval_minutes = _parse_interval(interval)
gaps = detect_session_gaps(klines, interval_minutes)
return {
"klines": klines,
"gap_analysis": {
"total_gaps": len(gaps),
"session_gaps": len([g for g in gaps if g["gap_type"] == "session_boundary"]),
"data_gaps": len([g for g in gaps if g["gap_type"] == "data_gap"]),
"details": gaps
}
}
def _parse_interval(interval: str) -> int:
"""Convert interval string to minutes."""
mapping = {
"1m": 1, "5m": 5, "15m": 15, "30m": 30,
"1h": 60, "2h": 120, "4h": 240, "1d": 1440
}
return mapping.get(interval, 60)
Gap Filling: When and How
TickDB does not automatically fill gaps with interpolated values. Interpolation introduces synthetic data that can distort statistical properties, especially for volatility and return calculations. Instead, TickDB provides two modes:
ignore_gap=false(default): Returns the actual data as it occurred, including gaps. Your strategy logic must handle gaps explicitly.ignore_gap=true: Returns only complete periods. Weekend gaps and holiday gaps are omitted from the series, which is useful for strategies that trade intraday only.
Dimension 5: Cross-Market Temporal Alignment
The Problem
A cross-asset strategy might combine US equity options, Treasury futures, and currency forwards. Each market has different trading hours:
- US equity options: 09:30–16:15 ET
- 10-Year Treasury futures: 18:00–17:00 ET (nearly 24-hour)
- EUR/USD forex: 24-hour, except weekends
When you align these time series, you need to know: which data points represent the same moment in time? A Treasury futures print at 17:00 ET and a forex print at 17:00 ET are simultaneous. A US equity print at 17:00 ET occurred after the equity market closed.
TickDB's Cross-Market Alignment
TickDB provides two tools for cross-market alignment:
Session metadata: Every symbol includes trading session information (
session_start,session_end,timezone). Use this to determine whether two timestamps fall within overlapping sessions.Alignment utilities: Helper functions to resample multiple time series to a common timestamp grid.
from datetime import datetime
import pandas as pd
def align_cross_market_klines(
kline_sets: dict[str, list[dict]],
target_interval: str = "1h",
timezone: str = "America/New_York"
) -> pd.DataFrame:
"""
Align multiple kline datasets to a common timestamp grid.
This is critical for multi-asset strategies where you need
synchronized data across equities, futures, and forex.
Args:
kline_sets: Dict of {symbol: kline_data}
target_interval: Target interval for alignment (e.g., "1h")
timezone: Timezone for output timestamps
Returns:
DataFrame with aligned close prices for all symbols
"""
dfs = []
for symbol, klines in kline_sets.items():
if not klines:
continue
df = pd.DataFrame(klines)
df["timestamp"] = pd.to_datetime(df["timestamp"])
df = df.set_index("timestamp")
df = df.sort_index()
# Resample to target interval, using last available price
# For OHLCV data, use the last close price
resampled = df["close"].resample(target_interval).last()
resampled.name = symbol
dfs.append(resampled)
if not dfs:
return pd.DataFrame()
# Align all series on a common index
aligned = pd.concat(dfs, axis=1)
# Forward-fill missing values within sessions, but preserve session gaps
aligned = aligned.ffill()
return aligned
def is_session_overlap(
ts1: datetime,
ts2: datetime,
symbol1_session: dict,
symbol2_session: dict
) -> bool:
"""
Check if two timestamps fall within overlapping trading sessions.
Returns True if both timestamps are within their respective
trading sessions, meaning the data points are contemporaneous.
"""
import zoneinfo
def ts_in_session(ts: datetime, session: dict) -> bool:
tz = zoneinfo.ZoneInfo(session["timezone"])
local_ts = ts.astimezone(tz)
start_parts = session["start"].split(":")
end_parts = session["end"].split(":")
start_hour, start_min = int(start_parts[0]), int(start_parts[1])
end_hour, end_min = int(end_parts[0]), int(end_parts[1])
start_minutes = start_hour * 60 + start_min
end_minutes = end_hour * 60 + end_min
ts_minutes = local_ts.hour * 60 + local_ts.minute
# Handle overnight sessions (e.g., crypto, some futures)
if end_minutes < start_minutes:
return ts_minutes >= start_minutes or ts_minutes <= end_minutes
else:
return start_minutes <= ts_minutes <= end_minutes
return ts_in_session(ts1, symbol1_session) and ts_in_session(ts2, symbol2_session)
# Example: Aligning equity with Treasury futures
if __name__ == "__main__":
equity_session = {
"start": "09:30",
"end": "16:00",
"timezone": "America/New_York"
}
futures_session = {
"start": "18:00",
"end": "17:00", # Overnight session
"timezone": "America/New_York"
}
# Test timestamp: 10:30 AM ET (both equity and futures are trading)
test_ts = datetime(2024, 1, 15, 10, 30, tzinfo=zoneinfo.ZoneInfo("America/New_York"))
overlap = is_session_overlap(test_ts, test_ts, equity_session, futures_session)
print(f"10:30 AM ET overlap: {overlap}") # True: futures still trading, equity open
# Test timestamp: 17:30 PM ET (equity closed, futures still trading)
test_ts2 = datetime(2024, 1, 15, 17, 30, tzinfo=zoneinfo.ZoneInfo("America/New_York"))
overlap2 = is_session_overlap(test_ts2, test_ts2, equity_session, futures_session)
print(f"17:30 PM ET overlap: {overlap2}") # False: equity closed
Verifying TickDB's Data Quality Pipeline
TickDB provides dedicated endpoints for verifying data quality at each stage of the pipeline.
| Verification need | Endpoint | What it returns |
|---|---|---|
| Corporate actions | GET /v1/reference/corporate-actions |
Split dates, dividend amounts, adjustment factors |
| Symbol metadata | GET /v1/symbols/available |
Exchange, timezone, session hours |
| Anomaly flags | GET /v1/market/anomalies |
Detected anomalies with severity levels |
| Kline gaps | GET /v1/market/kline with ignore_gap param |
Gap detection metadata |
def full_data_quality_report(symbol: str, start_date: str, end_date: str) -> dict:
"""
Generate a comprehensive data quality report for a symbol.
This function calls multiple TickDB endpoints to verify
each dimension of data quality.
"""
report = {"symbol": symbol, "period": f"{start_date} to {end_date}", "checks": {}}
# Check 1: Corporate actions
try:
actions = fetch_splits_and_dividends(symbol, start_date, end_date)
report["checks"]["corporate_actions"] = {
"status": "ok",
"split_count": len([a for a in actions.get("events", []) if a["type"] == "split"]),
"dividend_count": len([a for a in actions.get("events", []) if a["type"] == "dividend"]),
}
except Exception as e:
report["checks"]["corporate_actions"] = {"status": "error", "message": str(e)}
# Check 2: Symbol metadata and timezone
try:
api_key = os.environ.get("TICKDB_API_KEY")
resp = requests.get(
"https://api.tickdb.ai/v1/symbols/available",
headers={"X-API-Key": api_key},
params={"symbol": symbol},
timeout=(3.05, 10)
)
resp.raise_for_status()
symbol_data = resp.json().get("data", {})
report["checks"]["symbol_metadata"] = {
"status": "ok",
"exchange": symbol_data.get("exchange"),
"timezone": symbol_data.get("timezone"),
"session_start": symbol_data.get("session_start"),
"session_end": symbol_data.get("session_end"),
}
except Exception as e:
report["checks"]["symbol_metadata"] = {"status": "error", "message": str(e)}
# Check 3: Kline quality
try:
kline_result = fetch_klines_with_gap_analysis(symbol, "1h", 500, ignore_gap=False)
gaps = kline_result["gap_analysis"]
report["checks"]["kline_quality"] = {
"status": "ok" if gaps["data_gaps"] == 0 else "warning",
"total_bars": len(kline_result["klines"]),
"session_gaps": gaps["session_gaps"],
"data_gaps": gaps["data_gaps"],
"gap_details": gaps["details"] if gaps["data_gaps"] > 0 else []
}
except Exception as e:
report["checks"]["kline_quality"] = {"status": "error", "message": str(e)}
return report
# Generate a sample report
sample_report = full_data_quality_report("AAPL.US", "2023-01-01", "2024-01-01")
print("Data Quality Report:")
for check_name, result in sample_report["checks"].items():
status_icon = "✅" if result["status"] == "ok" else "⚠️" if result["status"] == "warning" else "❌"
print(f" {status_icon} {check_name}: {result['status']}")
Why This Matters for Your Strategy
The five dimensions we have examined — corporate actions, timestamp normalization, anomaly detection, missing data handling, and cross-market alignment — are not academic concerns. They are the difference between a backtest that forecasts reality and one that lies to you.
Consider the cumulative effect over a three-year backtest:
- A 2% dividend yield unaccounted for over three years inflates returns by approximately 6%.
- A single fat-finger print with a 20-standard-deviation move can double your estimated volatility.
- Weekend gaps treated as trading days inflate your apparent Sharpe ratio by masking the true variance of weekly returns.
The data cleaning pipeline is not a feature. It is the foundation on which every quantitative strategy stands. Without clean, aligned data, your strategy optimization, parameter tuning, and risk modeling are exercises in fitting noise.
Next Steps
If you are building a backtesting framework, start with TickDB's /v1/reference/corporate-actions endpoint to understand which symbols have experienced splits or dividends during your backtest period. Apply split adjustments before running any price-based strategy.
If you are running live strategies, verify timestamp alignment before deploying multi-market strategies. Use the session metadata from /v1/symbols/available to implement your own session-overlap checks.
If you are evaluating data quality, run the full_data_quality_report function on your target symbols before committing to a backtest period. The 30 seconds spent on quality verification can save hours of debugging a strategy that fails in live trading.
To explore TickDB's full API capabilities: visit tickdb.ai and sign up for a free API key. The free tier includes access to 10+ years of cleaned US equity OHLCV data, corporate action references, and symbol metadata.
If you are integrating with AI coding tools: search for and install the tickdb-market-data SKILL in your AI tool's marketplace to access TickDB capabilities directly from your development environment.
This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results.