SAS Phase2 Implementation Guide
SAS Phase2 Implementation Guide
D3 Order Book Snapshot System 1-second L2 snapshots · Imbalance ratio · Redis + TimescaleDB
D4 Derivatives Data Pipeline Funding rates · Open Interest · Liquidations · Long/Short ratios
D5 Sentiment & On-Chain Pipeline Fear & Greed · Glassnode · CryptoCompare NLP · Daily ingestion
D6 Data Quality Monitor & Auto- Gap detection · Auto-backfill · data_quality_events · DLQ alerts
Backfill
D7 Replay Engine Zero look-ahead MarketState iterator · Sole backtest data source
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
Phase 2 Overview
Phase 2 builds the data nervous system. It establishes every external data connection, persists all historical
and live market data to TimescaleDB, publishes real-time MarketState to Redis, and provides the Replay
Engine that all downstream backtesting will use. The 15 external data sources, compression policies, quality
monitoring, and auto-backfill logic are all built here.
Prerequisites Checklist
C Prerequisite Verification
h
e
c
k
☐ Phase 1 complete — all 8 deliverables docker compose ps → all 3 healthy. curl localhost:8000/health →
verified {status:'healthy'}
☐ Binance Testnet account active with API [Link] — login works. Testnet key + secret in .env
keys generated
☐ TimescaleDB tables exist (from Phase 1 psql → \dt → shows ohlcv, orderbook_snapshots, funding_rates,
migration) open_interest
☐ Redis accessible and responding to PING docker exec sas-cache redis-cli ping → PONG
☐ python-binance or ccxt library chosen and pip install python-binance OR pip install ccxt — add to [Link]
installed
☐ Coinglass, Glassnode, CryptoCompare API Keys from D4 of Phase 0 stored in .env (not committed to git)
keys in .env
☐ [Link] URL accessible from VPS curl [Link] → returns JSON with value field
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
Every downstream phase consumes MarketState objects. This is the central data structure of the entire system.
Phase 2 is responsible for building it correctly and publishing it to Redis on every candle close and every order
book update.
sas/data/[Link]
# sas/data/[Link]
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from decimal import Decimal
from typing import Optional
@dataclass
class OHLCVBar:
time: datetime
symbol: str
timeframe: str
open: Decimal
high: Decimal
low: Decimal
close: Decimal
volume: Decimal
@dataclass
class OrderBookState:
time: datetime
symbol: str
bid_volume_top5: Decimal
ask_volume_top5: Decimal
imbalance_ratio: float # bid_vol / ask_vol
best_bid: Decimal
best_ask: Decimal
spread_pct: float
is_stale: bool = False
@dataclass
class DerivativesState:
time: datetime
symbol: str
funding_rate: float
funding_z_score: float # vs 30-day rolling mean
open_interest: Decimal
oi_delta_4h: float # OI change over last 4 hours
long_short_ratio: float
is_stale: bool = False
@dataclass
class SentimentState:
time: datetime
fear_greed_index: int # 0–100
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
@dataclass
class MarketState:
"""Complete market snapshot at a specific timestamp.
# OHLCV per timeframe — keyed by '1m', '5m', '15m', '1h', '4h', '1d'
candles: dict[str, list[OHLCVBar]] = field(default_factory=dict)
# Derivatives data
derivatives: Optional[DerivativesState] = None
# Sentiment data
sentiment: Optional[SentimentState] = None
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
The REST connector performs the 2-year historical backfill for all timeframes (1m, 5m, 15m, 1h, 4h, 1d) for both
BTCUSDT and ETHUSDT. It uses an upsert pattern to prevent duplicate rows on restart, and a
checkpoint/resume mechanism so an interrupted backfill picks up exactly where it left off.
sas/data/binance_rest.py
sas/data/binance_rest.py
from __future__ import annotations
import asyncio
import json
from datetime import datetime, timezone, timedelta
from decimal import Decimal
from pathlib import Path
from typing import AsyncIterator
import asyncpg
from binance import AsyncClient
logger = get_logger(__name__)
class BinanceRestConnector:
"""Handles all REST-based data operations.
Responsibilities:
- Full historical OHLCV backfill with checkpoint/resume
- Periodic funding rate, OI, and liquidation fetches
- Gap-fill for missing candles detected by Data Quality Monitor
- All writes use UPSERT (ON CONFLICT DO NOTHING) to prevent duplicates
"""
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
# ── BACKFILL ──────────────────────────────────────────────────
async def run_full_backfill(self) -> None:
"""Run 2-year OHLCV backfill for all symbols + timeframes.
resume_from = [Link](key)
from_dt = [Link](resume_from) if resume_from \
else start_time
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
# ── CHECKPOINT ────────────────────────────────────────────────
def _load_checkpoint(self) -> dict:
if CHECKPOINT_FILE.exists():
return [Link](CHECKPOINT_FILE.read_text())
return {}
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
The WebSocket manager maintains persistent connections for live kline data and L2 order book snapshots. It
implements exponential backoff reconnection, a watchdog task that detects stale connections, and an
[Link] with backpressure to buffer bursts without memory exhaustion.
sas/data/websocket_manager.py
sas/data/websocket_manager.py
from __future__ import annotations
import asyncio
import json
import time
from datetime import datetime, timezone
from decimal import Decimal
from typing import Callable, Awaitable
import websockets
from [Link] import ConnectionClosed
logger = get_logger(__name__)
class WebSocketManager:
"""Manages persistent Binance WebSocket connections.
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
Implements:
- Exponential backoff reconnection (1s → 2s → 4s → 8s → 16s cap)
- Watchdog task that kills and restarts stale connections
- [Link] with maxsize=10,000 for backpressure
"""
def __init__(
self,
on_kline: KlineHandler,
on_orderbook: OrderBookHandler,
) -> None:
self._on_kline = on_kline
self._on_orderbook = on_orderbook
self._queue: [Link][dict] = [Link](maxsize=QUEUE_MAXSIZE)
self._running = False
self._last_message_ts: dict[str, float] = {} # stream_name -> timestamp
self._tasks: list[[Link]] = []
stream_path = '/'.join(streams)
url = f'{ws_base}?streams={stream_path}'
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
while self._running:
try:
[Link]('websocket_connecting', url=url[:60])
async with [Link](
url,
ping_interval=20,
ping_timeout=10,
close_timeout=5,
) as ws:
backoff = 1 # Reset on successful connection
[Link]('websocket_connected')
async for raw in ws:
if not self._running:
break
try:
msg = [Link](raw)
self._last_message_ts[[Link]('stream','')]= [Link]()
# Non-blocking put — drop if queue full (backpressure)
try:
self._queue.put_nowait(msg)
except [Link]:
[Link]('ws_queue_full_dropping_message')
except [Link]:
pass
except [Link]:
break
# ── WATCHDOG ─────────────────────────────────────────────────
async def _watchdog_loop(self) -> None:
while self._running:
await [Link](15)
now = [Link]()
for stream, last_ts in list(self._last_message_ts.items()):
age = now - last_ts
if age > STALE_THRESHOLD_SEC:
[Link]('websocket_stream_stale',
stream=stream, age_sec=round(age, 1))
# Phase 9 latency check is handled by DataQualityMonitor
await [Link](DataGapDetectedEvent(
source='websocket',
description=f'Stream {stream} stale for {age:.1f}s',
))
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
if '@kline_' in stream:
await self._on_kline(data)
elif '@depth' in stream:
await self._on_orderbook(data)
except [Link]:
continue # Normal — no messages in 1s
except [Link]:
break
except Exception as exc:
[Link]('queue_processor_error', error=str(exc))
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
The order book snapshot system captures L2 depth every time the WebSocket pushes an update
(approximately every 100ms), computes the bid/ask imbalance ratio, stores the snapshot in Redis with a 10-
second TTL for real-time access by the Phase 4 Scalp Engine, and persists snapshots to TimescaleDB for
backtesting.
sas/data/[Link]
sas/data/[Link]
from __future__ import annotations
import json
from datetime import datetime, timezone
from decimal import Decimal
import asyncpg
import [Link] as aioredis
logger = get_logger(__name__)
class OrderBookProcessor:
"""Processes live order book WebSocket messages.
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
snapshot = OrderBookState(
time = [Link]([Link]),
symbol = symbol,
bid_volume_top5 = bid_vol,
ask_volume_top5 = ask_vol,
imbalance_ratio = imbalance,
best_bid = best_bid,
best_ask = best_ask,
spread_pct = spread_pct,
)
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
The derivatives pipeline ingests funding rates (every 8 hours), open interest (every 15 minutes), liquidation
levels, and long/short ratios from Binance and Coinglass. All data is stored as TimescaleDB hypertables with
compression configured here (C5 correction). The Z-score calculation for funding rate normalization is pre-
computed on ingestion.
sas/data/[Link]
sas/data/[Link]
from __future__ import annotations
import asyncio
from datetime import datetime, timezone, timedelta
from decimal import Decimal
from statistics import mean, stdev
import asyncpg
import httpx
logger = get_logger(__name__)
class DerivativesPipeline:
"""Periodic ingestion of derivatives data.
Schedule:
- Funding rates: every 8 hours (or on each payment time)
- Open Interest: every 15 minutes
- Long/Short ratio: every 15 minutes (Coinglass)
- Liquidations: every 1 hour
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
))
self._tasks.append(asyncio.create_task(
self._oi_loop(), name='oi_loop'
))
self._tasks.append(asyncio.create_task(
self._ls_ratio_loop(), name='ls_ratio_loop'
))
[Link]('derivatives_pipeline_started')
rows = []
for item in data:
ts = [Link](item['fundingTime']/1000, tz=[Link])
rate = float(item['fundingRate'])
[Link]((ts, symbol, rate))
if rows:
# Compute Z-score vs rolling 30-day mean
rates = [r[2] for r in rows]
if len(rates) > 1:
mu = mean(rates)
sig = stdev(rates) or 0.0001
z = (rates[-1] - mu) / sig
else:
z = 0.0
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
latest_rate=rates[-1], z_score=round(z,3))
ts = [Link](data['time']/1000, tz=[Link])
oi = Decimal(data['openInterest'])
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
if not row:
return None
now = [Link]([Link])
stale = (now - row['time']).total_seconds() > 8.5 * 3600
return DerivativesState(
time = row['time'],
symbol = symbol,
funding_rate = float(row['funding_rate']),
funding_z_score= 0.0, # Computed by Phase 5 FA scorer
open_interest = oi_row['oi_value'] if oi_row else Decimal(0),
oi_delta_4h = 0.0, # Computed by Phase 5
long_short_ratio = 1.0,
is_stale = stale,
)
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
The sentiment pipeline runs daily ingestion tasks for the Fear & Greed index, Glassnode on-chain metrics
(exchange net flow, SOPR, MVRV Z-score), and CryptoCompare news with NLP sentiment scoring. All data is
stored with freshness metadata so downstream phases can detect stale values per the D4 thresholds.
sas/data/[Link]
sas/data/[Link]
from __future__ import annotations
import asyncio
from datetime import datetime, timezone, timedelta
import asyncpg
import httpx
logger = get_logger(__name__)
ALTERNATIVE_ME_URL = '[Link]
GLASSNODE_BASE = '[Link]
CRYPTOCOMPARE_BASE = '[Link]
class SentimentPipeline:
"""Daily ingestion of sentiment and on-chain data.
Schedule:
- Fear & Greed index: every 12 hours (daily updates)
- Glassnode metrics: every 24 hours at 01:00 UTC
- News sentiment: every 2 hours
PIT Note: All daily data is stored with its publication date.
Phase 18 feature engineering uses T-1 values for intraday signals
(today's data is not yet final when an intraday trade is taken).
"""
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
self._glassnode_loop(), name='glassnode'
))
self._tasks.append(asyncio.create_task(
self._news_loop(), name='news_sentiment'
))
[Link]('sentiment_pipeline_started')
latest = entries[0]
value = int(latest['value'])
ts = [Link](int(latest['timestamp']), tz=[Link])
if len(historical) > 5:
from statistics import mean, stdev
mu = mean(historical)
sig = stdev(historical) or 1.0
z = (value - mu) / sig
else:
z = 0.0
await [Link](
'''INSERT INTO system_events
(event_type, phase, severity, payload)
VALUES ('fear_greed_update', 'P2', 'INFO', $1)''',
{'value': value, 'timestamp': [Link](), 'z_score': z},
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
)
[Link]('fear_greed_stored', value=value, z_score=round(z,3))
# ── GLASSNODE ─────────────────────────────────────────────────
async def _glassnode_loop(self) -> None:
while True:
await [Link](
self._seconds_until_utc_hour(1) # Run at 01:00 UTC daily
)
if not settings.glassnode_api_key:
await [Link](3600)
continue
try:
await self._fetch_glassnode_metrics()
except Exception as exc:
[Link]('glassnode_error', error=str(exc))
await [Link](60 * 60 * 24) # Then every 24 hours
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
[Link]('news_sentiment_error', error=str(exc))
await [Link](60 * 60 * 2) # Every 2 hours
scores = []
for article in articles:
title = [Link]('title','').lower()
pos = sum(1 for w in POSITIVE if w in title)
neg = sum(1 for w in NEGATIVE if w in title)
score = (pos - neg) / max(pos + neg, 1)
[Link](score)
@staticmethod
def _seconds_until_utc_hour(target_hour: int) -> float:
now = [Link]([Link])
target = [Link](hour=target_hour, minute=0, second=0, microsecond=0)
if target <= now:
target += timedelta(days=1)
return (target - now).total_seconds()
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
The data quality monitor runs every 5 minutes, checking all critical data streams for gaps, staleness, and
latency breaches. Every anomaly is written to the data_quality_events table (C6). On OHLCV gap detection, it
automatically triggers a targeted backfill. On 3+ consecutive failures, it publishes a DataGapDetectedEvent to
the event bus.
sas/data/quality_monitor.py
sas/data/quality_monitor.py
from __future__ import annotations
import asyncio
from datetime import datetime, timezone, timedelta
from typing import TYPE_CHECKING
import asyncpg
if TYPE_CHECKING:
from [Link].binance_rest import BinanceRestConnector
logger = get_logger(__name__)
class DataQualityMonitor:
"""Runs every 5 minutes — checks all data streams for gaps and staleness.
On any anomaly:
1. Writes event to data_quality_events table
2. Triggers auto-backfill for OHLCV gaps
3. After 3 consecutive failures: publishes DataGapDetectedEvent
(Phase 9 may halt execution if this breaches latency threshold)
"""
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
def __init__(
self,
pool: [Link],
rest_connector: 'BinanceRestConnector',
) -> None:
self._pool = pool
self._rest = rest_connector
self._failures: dict[str, int] = {} # key -> consecutive failure count
self._task: [Link] | None = None
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
[Link]('data_quality_anomaly', key=key,
event_type=event_type, consecutive=count,
description=description)
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
await [Link](DataGapDetectedEvent(
source=key,
description=f'3+ consecutive failures: {description}',
))
[Link]('data_quality_critical', key=key, count=count)
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
Replay Engine
D7 Zero look-ahead MarketState iterator · Sole backtest data source
The Replay Engine is the most architecturally critical component in Phase 2. It is the sole data source for all
Phase 7 backtesting. Its defining guarantee: when replaying at timestamp T, it yields only data that was
available at timestamp T. This zero look-ahead guarantee is enforced by strict WHERE clause filtering — no
LEAD() functions, no future-looking window functions, no post-hoc corrections.
sas/data/replay_engine.py
sas/data/replay_engine.py
from __future__ import annotations
import asyncpg
logger = get_logger(__name__)
CANDLE_LOOKBACK = {
'1m': 200, # bars of history for each timeframe
'5m': 200,
'15m': 200,
'1h': 200,
'4h': 200,
'1d': 200,
}
class ReplayEngine:
"""Yields MarketState objects for backtesting.
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
Usage:
async for state in [Link]('BTCUSDT','4h', start, end):
# [Link] is the candle close time
# all state data is available as-of [Link]
context = market_structure_engine.evaluate(state)
"""
# Fetch all candle close times in range
async with self._pool.acquire() as conn:
timestamps = await [Link](
'''SELECT DISTINCT time FROM ohlcv
WHERE symbol=$1 AND timeframe=$2
AND time >= $3 AND time <= $4
ORDER BY time ASC''',
symbol, timeframe, start_time, end_time,
)
[Link]('replay_starting',
symbol=symbol, timeframe=timeframe,
candles=len(timestamps))
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
return MarketState(
timestamp = snapshot_time,
symbol = symbol,
candles = candles,
order_book = ob,
derivatives = deriv,
sentiment = sent,
atr = atr,
)
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
return OrderBookState(
time=row['time'], symbol=symbol,
bid_volume_top5=row['bid_volume_top5'],
ask_volume_top5=row['ask_volume_top5'],
imbalance_ratio=float(row['imbalance_ratio']),
best_bid=row['best_bid'], best_ask=row['best_ask'],
spread_pct=float(row['spread_pct']),
)
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
@staticmethod
def _compute_atr(
candles: dict[str, list[OHLCVBar]],
period: int = 14,
) -> dict[str, float]:
atr: dict[str, float] = {}
for tf, bars in [Link]():
if len(bars) < 2:
atr[tf] = 0.0
continue
trs = []
for i in range(1, min(period + 1, len(bars))):
high = float(bars[i].high)
low = float(bars[i].low)
prev_close = float(bars[i-1].close)
tr = max(high - low,
abs(high - prev_close),
abs(low - prev_close))
[Link](tr)
atr[tf] = sum(trs) / len(trs) if trs else 0.0
return atr
These tests are the most important in the entire Phase 2 test suite.
A Replay Engine with look-ahead bias invalidates every backtest result.
"""
import pytest
from datetime import datetime, timezone, timedelta
from [Link] import AsyncMock, MagicMock
class TestZeroLookAhead:
async def test_candles_strictly_before_snapshot_time(self, mock_pool):
"""No candle with time > snapshot_time must ever appear in state."""
snapshot_time = datetime(2025, 6, 15, 12, 0, tzinfo=[Link])
engine = ReplayEngine(mock_pool)
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
sas/data/market_state_publisher.py
sas/data/market_state_publisher.py
from __future__ import annotations
import asyncio
import json
from datetime import datetime, timezone
import asyncpg
import [Link] as aioredis
logger = get_logger(__name__)
class MarketStatePublisher:
"""Assembles and publishes MarketState on every candle close.
def __init__(
self,
pool: [Link],
redis: [Link],
ob_processor: OrderBookProcessor,
derivatives: DerivativesPipeline,
) -> None:
self._pool = pool
self._redis = redis
self._ob = ob_processor
self._deriv = derivatives
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
try:
state = await self._build(symbol, timeframe, close_time)
await self._publish_to_redis(state)
[Link]('market_state_published',
symbol=symbol, timeframe=timeframe,
close_time=str(close_time))
except Exception as exc:
[Link]('market_state_build_failed',
symbol=symbol, tf=timeframe, error=str(exc))
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
☐ 2-year OHLCV backfill complete for BTC + SELECT COUNT(*), timeframe FROM ohlcv GROUP BY timeframe
ETH, all 6 timeframes → ~500k+ rows per TF
☐ Checkpoint file shows all 12 combinations cat /app/data/.backfill_checkpoint.json → 12 keys all 'complete'
'complete'
☐ WebSocket connects and streams live klines docker logs sas-app | grep websocket_connected → appears on
without disconnect startup
☐ Order book snapshots appearing in SELECT COUNT(*) FROM orderbook_snapshots WHERE time >
TimescaleDB every ~100ms NOW() - INTERVAL '1 minute' → > 0
☐ Redis ob:BTCUSDT key returns fresh docker exec sas-cache redis-cli get ob:BTCUSDT → JSON with
imbalance_ratio imbalance_ratio field
☐ Redis ms:BTCUSDT:1m key updates on docker exec sas-cache redis-cli get ms:BTCUSDT:1m → JSON
every 1M candle close with recent timestamp
☐ Funding rates ingesting every 8 hours SELECT COUNT(*), MAX(time) FROM funding_rates → has recent
rows
☐ Open interest ingesting every 15 minutes SELECT COUNT(*), MAX(time) FROM open_interest → has recent
rows
☐ Fear & Greed index has at least 7 days of SELECT COUNT(*) FROM system_events WHERE
historical data event_type='fear_greed_update' → > 7
☐ Data quality monitor running and logging docker logs sas-app | grep quality_monitor → periodic log entries
every 5 minutes
☐ A simulated OHLCV gap triggers auto-backfill Run integration test: pytest tests/integration/test_quality_monitor.py -
v
☐ Replay Engine anti-look-ahead tests pass pytest tests/unit/test_replay_engine.py -v → ALL 3 look-ahead tests
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
PASS
☐ No data_quality_events with unresolved gaps SELECT * FROM data_quality_events WHERE resolved_at IS NULL
after 24h operation → empty
☐ CI pipeline green after all Phase 2 code GitHub Actions → latest push → all jobs green
added
Market Structure Engine [Link] — dict of Candles must be in chronological order, no duplicates,
OHLCVBar lists per timeframe no gaps
BOS / CHoCH Detection [Link]['4h'] and 200+ bars of 4H and 1D history required for reliable
['1d'] lookback bars swing detection
Order Block Identification [Link] — last N Candles must represent exact historical sequence —
bars before a BOS no reordering
Scalp OB Imbalance MarketState.order_book.imbalanc Must be < 10 seconds old. Stale OB = invalid scalp
Check e_ratio entry condition
ATR for SL Calculation [Link] — pre-computed ATR must use only candles available at
ATR per timeframe snapshot_time (no look-ahead)
Replay (Phase 7) [Link]() — the sole Zero look-ahead tests must be passing. Any failure
backtest data source invalidates all backtests.
Phase 2 Sign-Off
Phase 2 Start Date Phase 2 End Date Actual Hours Spent
SAS TRADING SYSTEM · PHASE 2 · DATA ACQUISITION & MARKET STATE · v1.0 · 2026
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code
SAS · PHASE 2 · DATA ACQUISITION & MARKET STATE FOUNDATION ROUND 1 · FOUNDATION
SAS Phase 2 Implementation Guide · 2026 All 7 Deliverables · Full Production Code