Files
hyper/scripts/gap_detector.py
DiTus 7d702e9cbd Migrate data pipeline from SQLite to PostgreSQL + Docker setup
- Add db.py PostgreSQL abstraction layer (connection, upsert, table mgmt)
- Replace sqlite3 with psycopg2 in: live_candle_fetcher, resampler,
  data_fetcher, fetch_history, import_csv, indicators, base_strategy
- Sanitize table names (colons -> underscores) for PostgreSQL compat
- Replace INSERT OR REPLACE with ON CONFLICT upserts
- Replace pandas to_sql() with batch upsert_candles()
- Add scripts: resampler_loop, gap_detector, backup_runner, cron_scheduler
- Add migrate_sqlite_to_pg.py for one-time data migration
- Add Dockerfile, docker-compose.yml, supervisord.conf
- Add postgres/postgresql.conf tuned for 4GB RAM (Synology DS1513+)
- Add .dockerignore, .env.docker.example, secrets template
- Update requirements.txt (psycopg2-binary), .gitignore
- Add MIGRATION_PLAN.md with full plan and todo list
2026-07-30 22:14:31 +02:00

138 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""
Gap Detector
Detects missing 1-minute candle data in the PostgreSQL database and
backfills gaps by fetching historical data from the Hyperliquid HTTP API.
Designed to run as a periodic cron job (hourly) inside the Docker container.
"""
import argparse
import logging
import os
import sys
import time
from datetime import datetime, timedelta, timezone
import pandas as pd
from hyperliquid.info import Info
from hyperliquid.utils import constants
from logging_utils import setup_logging
from db import get_connection, sanitize_table_name, upsert_candles
WATCHED_COINS = [
"BTC", "ETH", "SOL", "BNB", "HYPE", "SUI",
"xyz:BRENTOIL", "xyz:CL", "xyz:GOLD", "xyz:SILVER",
"mkts:USTECH", "xyz:XYZ100"
]
def detect_and_fill_gaps(coin, conn):
"""Detect gaps in the 1m data for a coin and backfill them."""
table_name = sanitize_table_name(coin, "1m")
now = datetime.now(timezone.utc)
start = now - timedelta(hours=24)
query = f'SELECT timestamp_ms FROM "{table_name}" WHERE timestamp_ms >= %s ORDER BY timestamp_ms'
df = pd.read_sql(query, conn, params=(int(start.timestamp() * 1000),))
if df.empty:
logging.info(f"No data for {coin} in the last 24 hours, skipping gap detection")
return
existing_timestamps = set(df['timestamp_ms'].tolist())
# Generate expected timestamps (every minute)
expected_timestamps = set()
current = start
while current <= now:
expected_timestamps.add(int(current.timestamp() * 1000))
current += timedelta(minutes=1)
gaps = expected_timestamps - existing_timestamps
if not gaps:
logging.info(f"No gaps found for {coin}")
return
logging.info(f"Found {len(gaps)} gaps for {coin}, backfilling...")
# Find contiguous gap ranges
sorted_gaps = sorted(gaps)
gap_ranges = []
gap_start = sorted_gaps[0]
gap_end = sorted_gaps[0]
for ts in sorted_gaps[1:]:
if ts == gap_end + 60000:
gap_end = ts
else:
gap_ranges.append((gap_start, gap_end + 60000))
gap_start = ts
gap_end = ts
gap_ranges.append((gap_start, gap_end + 60000))
info = Info(constants.MAINNET_API_URL, skip_ws=True)
for gap_start_ms, gap_end_ms in gap_ranges:
logging.info(
f"Backfilling gap for {coin}: "
f"{datetime.fromtimestamp(gap_start_ms/1000, tz=timezone.utc)} "
f"to {datetime.fromtimestamp(gap_end_ms/1000, tz=timezone.utc)}"
)
current_start = gap_start_ms
while current_start < gap_end_ms:
try:
batch = info.candles_snapshot(coin, "1m", current_start, gap_end_ms)
if not batch:
break
records = []
for candle in batch:
records.append((
datetime.fromtimestamp(candle['t'] / 1000, tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S'),
candle['t'],
candle.get('o'), candle.get('h'), candle.get('l'), candle.get('c'),
candle.get('v'), candle.get('n')
))
upsert_candles(conn, table_name, records)
last_ts = batch[-1]['t']
if last_ts < current_start:
break
current_start = last_ts + 1
time.sleep(0.5)
except Exception as e:
logging.error(f"Error backfilling gap for {coin}: {e}")
break
logging.info(f"Gap backfilling complete for {coin}")
def main():
parser = argparse.ArgumentParser(description="Detect and fill gaps in 1m candle data.")
parser.add_argument("--log-level", default="normal", choices=['off', 'normal', 'debug'])
args = parser.parse_args()
setup_logging(args.log_level, 'GapDetector')
conn = get_connection()
for coin in WATCHED_COINS:
try:
detect_and_fill_gaps(coin, conn)
except Exception as e:
logging.error(f"Error detecting gaps for {coin}: {e}")
conn.close()
logging.info("Gap detection complete!")
if __name__ == "__main__":
main()