Version: 2.2
Last Updated: May 31, 2026
Status: Production (integrity-monitor era — Findings #258/#290/#289.a; watchdog deleted #290, prices-backfill decommissioned #289)
This document describes the comprehensive data integrity system that ensures database completeness and enables full trade audit trails.
| Principle | Implementation |
|---|---|
| No Data Left Behind | All gaps detected and logged, backfilled when possible |
| Complete Audit Trail | Every trade recorded with full IBKR + system context |
| Automatic Recovery | Services auto-restart, gaps auto-detected on startup |
| Future-Proof | Schema designed for comprehensive analysis |
| Position Sync | DB and IBKR positions reconciled on startup (Jan 11, 2026) |
| Exit Verification | Every exit verified against IBKR before marking closed |
| Transactional Writes | Collectors use transactional inserts with rollback on errors |
┌─────────────────────────────────────────────────────────────────┐
│ DATA INTEGRITY SYSTEM │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Integrity │ │ Gap │ │ Backfill │ │
│ │ Monitor │──▶│ Detection │──▶│ Scheduler │ │
│ │ (5 min) │ │ Engine │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ PostgreSQL Database │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │ │
│ │ │data_gaps│ │trades │ │trade_audit_log │ │ │
│ │ └─────────┘ └─────────┘ └─────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Note: the legacy
Watchdog Service (5 min)box above is now the integrity monitor (watchdog deleted #290; replaced by polymarket-integrity-monitor.service). See §2.
Phase 3 of Database Integrity Audit (Finding #258). The previous v2 watchdog (
watchdog.py) and the never-properly-deployed v3 watchdog (watchdog_v3.py) were both decommissioned. Thedata_gapstable is retained for historical reference but is no longer written to (all 804 rows were tombstones withstatus='deleted'/gap_type='incorrect_source').The current integrity story is in
src/data_tracker/integrity_monitor.pywriting tointegrity_statustable, surfaced by/api/integrityon the Database tab (Phase 4 of the audit, Finding #259).
| Component | Path | Role |
|---|---|---|
| Monitor | src/data_tracker/integrity_monitor.py |
Long-running daemon. 5-minute internal loop. Evaluates declarative rules per cycle (live count: GET /api/integrity → summary.total; 85 as of Sep 26, 2026 probe — history: was ~51 pre-#383 growth). |
| Systemd unit | deploy/systemd/polymarket-integrity-monitor.service |
Type=simple, Restart=always, internal loop. |
| State table | integrity_status (migration 027) |
One row per rule. UPSERT on every cycle; tracks last_status_change_at + consecutive_failures. |
| Snapshot file | /var/log/polymarket/integrity_status.json |
Atomic-written every cycle. Read by /api/integrity for fast dashboard load. |
| Alerts | AlertCategory.DATA_INTEGRITY via src/earnings/alerts.py |
HIGH/CRITICAL transitions trigger Telegram. Rate-limit + dedup + circuit breakers inherited from AlertManager. |
| Dashboard surface | Database tab in earnings_dashboard/templates/dashboard.html |
4 panes (Freshness / Coverage / Services & Pipelines / Storage & Retention). Polls /api/integrity every 30 s. |
The monitor evaluates summary.total rules (live via GET /api/integrity; src/data_tracker/integrity_monitor.py — 85 as of Sep 26, 2026 probe; history: was ~51 before later rule additions) per cycle organised into 6 categories
(historical progression: was 37 before May 10 LATE morning — Finding #289.a added 3 new rules:
systemd_timer:polymarket-api-health.timer, systemd_last_run:polymarket-api-health.service,
and artifact:api_health_latest_json; was 36 before #291's artifact:trades_backfill_state;
was 34 before #269 (May 9 added trades-backfill timer + last_run); was 28
before Apr 27 evening when #268 added 6 systemd_last_run:* rules).
Subsequent additions (3 etfstack freshness:* rules +
etfstack-signal / etfstack-execute / ibgateway-paper-weekly-reconnect /
polymarket-strategy-analysis systemd_last_run:* rules brought it to ~47;
Aug 21 2026 the 4 archive:orderbook_* rules of #383 bring the current
total to ~51).
| Category | Count | What it checks |
|---|---|---|
freshness |
8 | MAX(time) vs expected interval. 5 core-table rules (e.g. price_snapshots ≤ 10 min, markets ≤ 60 min) + 3 etfstack rules (etfstack_signals, etfstack_target_weights, etfstack_equity_curve). |
null_rate |
5 | NULL % on tracked-quality columns scoped to post-fix-deploy windows. Min-rows floor avoids alerting on small batches. |
systemd |
25 | (a) 5 always-on services + integrity-monitor self-check (systemd:<unit>); (b) 8 timer freshness rules (systemd_timer:<timer>, includes polymarket-trades-backfill.timer from #269 + polymarket-api-health.timer from #289.a); (c) 12 batch-service exit-result rules (systemd_last_run:<unit>, Finding #268 baseline + #269 trades-backfill + #289.a api-health + later etfstack / strategy-analysis / paper-reconnect additions) for polymarket-backup, polymarket-resolver, polymarket-trade-reconcile, polymarket-equity-bars, polymarket-data-backfill, polymarket-earnings-sync, polymarket-trades-backfill, polymarket-api-health, etfstack-signal, etfstack-execute, ibgateway-paper-weekly-reconnect, polymarket-strategy-analysis. The last group reads Result + ExecMainStatus + ExecMainExitTimestamp so a unit's most-recent failed exit is monitored even when the unit is currently inactive(dead) between timer fires. |
artifact |
4 | File-mtime checks: ibkr_snapshot.json (5 min), strategy_analysis_report.json (14 d), trades_backfill_state.json (9 d, #291), and api_health_latest_json (26 h, #289.a — belt-and-suspenders for the #287 "ran but didn't write" pattern). |
storage |
8 | Hypertable size vs warn/fail thresholds (4 rules; orderbook_snapshots re-armed at 12 GB for the OPS-1 post-archive steady state), plus the 4 archive:orderbook_* rules guarding the lossless archive pipeline (#383): proof-gate results, timer progress, Spaces reachability, monthly spot re-verification. Runbook: docs/OPERATIONS.md §6 "Hypertable retention & archive". |
meta |
1 | Monitor self-heartbeat. |
Rules are declared in _build_rules() in integrity_monitor.py. Add new
rules by appending a Rule(name, category, severity, description, evaluator).
Why three systemd rule patterns instead of one:
systemd:<service> — checks is-active only. Right for always-on
services (earnings-trader, polymarket-dashboard, etc.) that should
always be running.systemd_timer:<timer> — checks LastTriggerUSec against an expected
cadence. Right for "did the timer fire recently?" — but does not
catch failed runs.systemd_last_run:<service> — checks Result + ExecMainStatus of
the last invocation. Right for batch services where is-active is
inactive(dead) between timer fires. This is the one that catches
OOM kills, exit-code≠0, signal-kill, timeout, etc. — added Apr 27,
2026 evening (Finding #268) after polymarket-backup.service was
OOM-killed three nights running with zero alerts.| Severity | Telegram? | Use for |
|---|---|---|
CRITICAL |
YES | Production-data-loss imminent (collector down, hypertable >1.5× threshold) |
HIGH |
YES | Significant integrity issue (NULL-rate regression, timer fired > 24h late) |
WARNING |
NO (dashboard only) | Notable but not urgent (30-day NULL-rate drift) |
INFO |
NO | Heartbeat / informational |
Per operator decision Q3 (Phase 1 + Phase 3 alert policy): Telegram pings only on HIGH/CRITICAL transitions. WARNING/INFO are dashboard-only.
ok → fail/warn ⇒ Telegram "TRIGGERED" alert (HIGH/CRITICAL only)
fail/warn → ok ⇒ Telegram "RESOLVED" alert (HIGH/CRITICAL only)
unknown → any ⇒ silent (rule has insufficient data, e.g. small post-fix window)
ok → unknown ⇒ silent (transient evaluator failure)
The unknown status exists specifically to handle "post-fix forward-only
window has 0-2 rows yet, can't evaluate" — graceful fallback that doesn't
fire a false alarm.
The §E blind spot from Finding #287. Three Dome-era backfill services
(polymarket-orderbook-backfill, polymarket-prices-backfill,
polymarket-dome-prices-backfill) were enabled+active+running for 12 days
post-Apr-28 Dome EOL while accomplishing zero useful work — their state
files stayed frozen at 5,735/5,735 and the cycle loop just exited
"all done" every hour. Existing freshness:* rules caught the DATA-side
problem (table not being written) for SOLE-writer services within
10-60 min, but not the COMPUTE-side waste (service active but
producing nothing) when REDUNDANT writers existed (Polymarket-native
consumers were independently populating the same target tables, so
freshness rules stayed clean).
The defense: any long-running daemon collector that maintains a state
file MUST also have an artifact:<state-file> rule. Frozen state file =
silent compute waste, even when the service appears active+running.
Currently in scope (May 10, 2026):
| Service | State file | mtime threshold | Rule |
|---|---|---|---|
polymarket-trades-backfill.service (timer-driven, Sun 06:00 ET) |
[hidden]/trades_backfill_state.json |
9 days (8d cadence + 1d slack) | artifact:trades_backfill_state |
Currently NOT in scope and why: the 3 always-on daemon collectors
(polymarket-collector.service, polymarket-live-trades.service,
polymarket-websocket.service) do NOT maintain state files; each is the
SOLE writer of its target table(s), so the existing freshness:* rules
already alarm 10-60 min after they'd silently die. A progress:* rule
attempting to attribute writes to a specific service was considered and
rejected as duplicating freshness coverage in the current architecture
(see Finding #291 RESOLVED entry for the design discussion).
Checklist for adding new long-running daemon collectors: see the
services=() array comment in deploy/deploy_from_github.sh.
| Data Type | Source | Notes |
|---|---|---|
| Trades | Polymarket data-api /trades?market=<cid> (Findings #287/#288, May 10, 2026 — formerly Dome /orders, EOL'd Apr 28, 2026) |
Historical trades; backfill via src/data_tracker/collectors/backfill_trades.py (smart Option A — weekly Sunday timer) |
| Prices | Live capture only via polymarket-collector.service (continuous_collector — gamma + clob) + polymarket-live-trades.service + scripts/backfill_post_event_snapshots_from_trades.py (Finding #246) |
Dedicated historical-prices backfill services removed Finding #289 (prices_backfill_service Gamma endpoint not in new docs; dome_prices_backfill deprecated by Dome EOL) |
| Orderbooks | Live only via polymarket-collector.service + polymarket-websocket.service (WSS market channel). No historical orderbook backfill after Dome /orderbooks EOL (Finding #287) — Polymarket has no direct historical-orderbook endpoint; deferred for Phase 3 simulator review. |
Live capture is reliable (orderbook_snapshots last write usually <1 min ago per freshness:orderbook_snapshots integrity rule) |
This service (prices_backfill_service.py + polymarket-prices-backfill.service)
was decommissioned during the Dome → Polymarket migration. The Gamma endpoint
it used (gamma-api.polymarket.com/markets/{cid}/prices) is not in the new
Polymarket API docs and was logging "Saved: 0 prices" cycling. Live price
capture continues via polymarket-collector.service (continuous_collector —
gamma + clob) and polymarket-live-trades.service (data-api /trades poller),
with scripts/backfill_post_event_snapshots_from_trades.py (Finding #246)
covering the post-event historical fill-in. price_snapshots table freshness
is monitored by freshness:price_snapshots integrity rule (10-min threshold).
If a future need for historical-price backfill arises, the Polymarket replacement
is clob.polymarket.com/prices-history?market=<token>&startTs=...&endTs=...
(rate limit 1,000 req/10s — 100 q/s).
Location: src/data_tracker/collectors/backfill_trades.py
Systemd Service: polymarket-trades-backfill.service
Reliability Notes:
- Backfill inserts into trade_dedupe first for global idempotency
- Backfill state file writes are atomic (temp + fsync + rename)
Uses Polymarket data-api /trades?market=<cid> to backfill historical trade data (Finding #288 migration; formerly Dome /orders until Apr 28, 2026 EOL).
Every trade executed through the system is recorded with comprehensive data from:
| Field | Description |
|---|---|
ibkr_account_id |
IBKR account identifier |
ibkr_perm_id |
Permanent order ID (unique across sessions) |
ibkr_entry_exec_id |
Execution ID for entry |
ibkr_exit_exec_id |
Execution ID for exit |
entry_execution_exchange |
Where order executed (NYSE, NASDAQ, etc.) |
entry_time_in_force |
GTC, DAY, IOC, etc. |
entry_fill_details |
JSON array of fills |
entry_time_to_fill_ms |
Milliseconds to first fill |
| Field | Description |
|---|---|
conviction_at_entry |
Polymarket conviction (0-1) |
conviction_source |
Orderbook method used |
signal_generated_at |
When signal was created |
time_signal_to_entry_ms |
Latency from signal to execution |
competing_signals |
Other signals we didn't take |
portfolio_state_at_entry |
Cash, positions, exposure |
position_sizing_details |
How size was calculated |
risk_parameters |
Stop distance, max loss, etc. |
| Field | Description |
|---|---|
bid_at_entry |
Stock bid price |
ask_at_entry |
Stock ask price |
last_price_at_entry |
Stock last trade |
volume_at_entry |
Stock volume |
pm_best_bid_at_entry |
Polymarket YES bid |
pm_best_ask_at_entry |
Polymarket YES ask |
pm_bid_depth_at_entry |
Polymarket bid depth |
| Field | Description |
|---|---|
actual_earnings_time |
When earnings announced |
actual_eps |
Actual EPS reported |
consensus_eps |
Consensus estimate |
eps_surprise |
Actual - Consensus |
pm_resolution_price |
What Polymarket resolved to |
pm_resolved_at |
Resolution timestamp |
| Field | Description |
|---|---|
limit_buffer_pct |
Tiered buffer (0.1%, 0.2%, 0.3%) |
entry_expected_price |
Limit price or mid |
entry_slippage_vs_expected |
Actual vs expected |
entry_limit_converted_to_market |
Limit timeout? |
entry_market_impact_pct |
Price moved due to order |
Note (core topology): the two tables detailed below (
data_gaps,trade_audit_log) are only part of the schema. The current core tables also includeearnings_markets,trading_positions,polymarket_opportunity_log,equity_bars_1m(~1.9M bars / 419 symbols),orderbook_snapshots,price_snapshots,markets, andintegrity_status. (data_gapsis retained for historical reference but no longer written — see §2.)
CREATE TABLE data_gaps (
id SERIAL PRIMARY KEY,
table_name VARCHAR(100) NOT NULL,
condition_id VARCHAR(100),
gap_start TIMESTAMPTZ NOT NULL,
gap_end TIMESTAMPTZ NOT NULL,
gap_type VARCHAR(50) DEFAULT 'collection_outage',
description TEXT,
records_affected INTEGER,
status VARCHAR(20) DEFAULT 'pending',
backfill_attempts INTEGER DEFAULT 0,
last_backfill_attempt TIMESTAMPTZ,
backfill_error TEXT,
corrected_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
See migration: src/data_tracker/migrations/005_enhanced_trade_audit.sql
Key additions in v2: - Full IBKR execution details - Fill details as JSONB - Portfolio state snapshots - Risk parameters - Slippage analysis fields - Earnings outcome data
# From project root
cd [hidden]
# Run migration
source .env
psql $DATABASE_URL -f src/data_tracker/migrations/005_enhanced_trade_audit.sql
# View real-time
journalctl -u polymarket-integrity-monitor -f
# Check for failed rules
journalctl -u polymarket-integrity-monitor | grep -E "STATUS_FAIL|alert"
# Check rule-evaluation cycle health
journalctl -u polymarket-integrity-monitor | grep "rule cycle"
# Live API view (preferred)
curl -s http://127.0.0.1:8080/api/integrity | python3 -m json.tool
Pre-Finding-#258 (Apr 27, 2026) the equivalent commands targeted
polymarket-watchdog.service(v2) andpolymarket-watchdog-v3.service. Both services were decommissioned per Finding #260c; the unit files andsrc/data_tracker/watchdog{,_v3}.pysource were deleted per Finding #290 (May 10, 2026). Thedata_gapstable is retained for historical reference but no longer written to.
-- Recent gaps
SELECT table_name, gap_start, gap_end, status, description
FROM data_gaps
WHERE created_at > NOW() - INTERVAL '7 days'
ORDER BY gap_start DESC;
-- Pending backfills
SELECT * FROM data_gaps WHERE status = 'pending';
-- Gap statistics
SELECT
status,
COUNT(*) as count,
AVG(EXTRACT(EPOCH FROM (gap_end - gap_start))/60) as avg_duration_min
FROM data_gaps
GROUP BY status;
-- Check trade completeness
SELECT
symbol,
entry_time,
CASE WHEN ibkr_perm_id IS NOT NULL THEN '✓' ELSE '✗' END as has_ibkr_id,
CASE WHEN entry_fill_details IS NOT NULL THEN '✓' ELSE '✗' END as has_fills,
CASE WHEN portfolio_state_at_entry IS NOT NULL THEN '✓' ELSE '✗' END as has_portfolio,
CASE WHEN position_sizing_details IS NOT NULL THEN '✓' ELSE '✗' END as has_sizing
FROM trade_audit_log
ORDER BY entry_time DESC
LIMIT 20;
| Service | Description | Port | Check |
|---|---|---|---|
polymarket-integrity-monitor |
Health monitoring (replaces v2/v3 watchdog per Finding #258 / #290) | - | systemctl status + curl /api/integrity |
polymarket-trades-backfill |
Trades backfill (timer-driven Sun 06:00 ET — Finding #269) | - | systemctl status |
polymarket-collector |
Real-time data | - | systemctl status |
Note:
polymarket-prices-backfill.servicewas DECOMMISSIONED (Finding #289 — see §3.2). The example below shows the generic install pattern using the current integrity-monitor unit instead.
# Copy service files
sudo cp deploy/systemd/polymarket-integrity-monitor.service /etc/systemd/system/
# Reload and enable
sudo systemctl daemon-reload
sudo systemctl enable polymarket-integrity-monitor
sudo systemctl start polymarket-integrity-monitor
polymarket-integrity-monitor.service) detects startup staleness via freshness:* rules (watchdog deleted #290; replaced by polymarket-integrity-monitor)/api/integrity + dashboard (HIGH/CRITICAL → Telegram)freshness:* rules (watchdog deleted #290; replaced by polymarket-integrity-monitor)Restart=always restarts the collector processintegrity_status (data_gaps no longer written — see §2)reconciliation_status = 'orphaned'Close position in DB with exit_reason = 'orphaned_reconciliation'
External IBKR Position (in IBKR but not DB):
external_trades tableRequires manual investigation and resolution
Position Mismatch (share counts differ):
# Check position reconciliation log
psql $DATABASE_URL -c "SELECT * FROM position_reconciliation_log ORDER BY run_time DESC LIMIT 5"
# Check external trades
psql $DATABASE_URL -c "SELECT * FROM external_trades WHERE resolved = FALSE"
# Check for orphaned positions
psql $DATABASE_URL -c "SELECT symbol, reconciliation_status FROM trading_positions WHERE status = 'open'"
# Check pending gaps
psql $DATABASE_URL -c "SELECT * FROM data_gaps WHERE status = 'pending'"
# Mark gap as backfilled (after manual intervention)
psql $DATABASE_URL -c "UPDATE data_gaps SET status = 'backfilled', corrected_at = NOW() WHERE id = <gap_id>"
-- Data freshness
SELECT
'orderbook_snapshots' as table_name,
MAX(time) as latest,
NOW() - MAX(time) as age
FROM orderbook_snapshots
UNION ALL
SELECT
'price_snapshots',
MAX(time),
NOW() - MAX(time)
FROM price_snapshots
UNION ALL
SELECT
'trades',
MAX(time),
NOW() - MAX(time)
FROM trades;
-- Coverage by hour (last 24h)
SELECT
DATE_TRUNC('hour', time) as hour,
COUNT(*) as records
FROM price_snapshots
WHERE time > NOW() - INTERVAL '24 hours'
GROUP BY hour
ORDER BY hour;
| File | Purpose |
|---|---|
src/data_tracker/integrity_monitor.py |
Health monitoring (replaces deleted watchdog{,_v3}.py per Finding #258 / #290) |
src/data_tracker/collectors/backfill_trades.py |
Trades backfill (timer-driven; Polymarket data-api per Finding #288) |
src/data_tracker/migrations/005_enhanced_trade_audit.sql |
Schema migration |
trades_backfill_state.json |
Trades backfill state (in project root) |
| Table | Field | Values |
|---|---|---|
data_gaps |
status |
pending, scheduled, backfilling, backfilled, failed, no_source, deleted |
trade_audit_log |
status |
pending, open, closed, cancelled |