- 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
133 lines
4.0 KiB
Python
133 lines
4.0 KiB
Python
"""
|
|
PostgreSQL database abstraction layer for the Hyperliquid trading toolkit.
|
|
|
|
Provides a thin wrapper around psycopg2 to centralize database operations,
|
|
handle table name sanitization, and abstract SQL dialect differences
|
|
from the SQLite-based codebase.
|
|
"""
|
|
|
|
import os
|
|
import psycopg2
|
|
from psycopg2.extras import execute_values
|
|
|
|
PG_CONN_STR = os.environ.get(
|
|
"PG_CONN_STR",
|
|
"postgresql://hyper:hyper@localhost:5432/hyper"
|
|
)
|
|
|
|
|
|
def get_connection():
|
|
"""Return a new psycopg2 connection to the PostgreSQL database."""
|
|
return psycopg2.connect(PG_CONN_STR)
|
|
|
|
|
|
def sanitize_table_name(coin, timeframe):
|
|
"""
|
|
Sanitize a coin/timeframe pair into a PostgreSQL-safe table name.
|
|
|
|
Replaces colons with underscores (e.g., 'xyz:BRENTOIL' -> 'xyz_BRENTOIL')
|
|
to ensure compatibility with PostgreSQL identifier rules.
|
|
"""
|
|
return f"{coin.replace(':', '_')}_{timeframe}"
|
|
|
|
|
|
def create_candle_table(conn, table_name):
|
|
"""
|
|
Create a candle table if it does not already exist.
|
|
|
|
Schema matches the original SQLite layout:
|
|
datetime_utc, timestamp_ms (PK), open, high, low, close, volume, number_of_trades
|
|
Also creates an index on datetime_utc for time-range queries.
|
|
"""
|
|
with conn.cursor() as cur:
|
|
cur.execute(f'''
|
|
CREATE TABLE IF NOT EXISTS "{table_name}" (
|
|
datetime_utc TIMESTAMP,
|
|
timestamp_ms BIGINT PRIMARY KEY,
|
|
open REAL,
|
|
high REAL,
|
|
low REAL,
|
|
close REAL,
|
|
volume REAL,
|
|
number_of_trades INTEGER
|
|
)
|
|
''')
|
|
cur.execute(
|
|
f'CREATE INDEX IF NOT EXISTS "idx_{table_name}_time" ON "{table_name}"(datetime_utc)'
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def upsert_candles(conn, table_name, records):
|
|
"""
|
|
Batch upsert candle records using PostgreSQL ON CONFLICT.
|
|
|
|
Args:
|
|
conn: psycopg2 connection
|
|
table_name: sanitized table name (e.g., 'BTC_1m')
|
|
records: list of tuples (datetime_utc, timestamp_ms, open, high,
|
|
low, close, volume, number_of_trades)
|
|
|
|
Returns:
|
|
Number of records upserted.
|
|
"""
|
|
if not records:
|
|
return 0
|
|
|
|
with conn.cursor() as cur:
|
|
execute_values(
|
|
cur,
|
|
f'''
|
|
INSERT INTO "{table_name}"
|
|
(datetime_utc, timestamp_ms, open, high, low, close, volume, number_of_trades)
|
|
VALUES %s
|
|
ON CONFLICT (timestamp_ms) DO UPDATE SET
|
|
datetime_utc = EXCLUDED.datetime_utc,
|
|
open = EXCLUDED.open,
|
|
high = EXCLUDED.high,
|
|
low = EXCLUDED.low,
|
|
close = EXCLUDED.close,
|
|
volume = EXCLUDED.volume,
|
|
number_of_trades = EXCLUDED.number_of_trades
|
|
''',
|
|
records,
|
|
page_size=1000
|
|
)
|
|
conn.commit()
|
|
return len(records)
|
|
|
|
|
|
def get_last_timestamp(conn, table_name):
|
|
"""Return the most recent timestamp_ms from a table, or None."""
|
|
with conn.cursor() as cur:
|
|
cur.execute(f'SELECT MAX(timestamp_ms) FROM "{table_name}"')
|
|
result = cur.fetchone()
|
|
return result[0] if result and result[0] is not None else None
|
|
|
|
|
|
def get_table_count(conn, table_name):
|
|
"""Return the total row count of a table."""
|
|
with conn.cursor() as cur:
|
|
cur.execute(f'SELECT COUNT(*) FROM "{table_name}"')
|
|
return cur.fetchone()[0]
|
|
|
|
|
|
def table_exists(conn, table_name):
|
|
"""Check if a table exists in the database."""
|
|
with conn.cursor() as cur:
|
|
cur.execute(
|
|
"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = %s)",
|
|
(table_name,)
|
|
)
|
|
return cur.fetchone()[0]
|
|
|
|
|
|
def get_table_columns(conn, table_name):
|
|
"""Return a list of column names for a table."""
|
|
with conn.cursor() as cur:
|
|
cur.execute(
|
|
"SELECT column_name FROM information_schema.columns WHERE table_name = %s",
|
|
(table_name,)
|
|
)
|
|
return [row[0] for row in cur.fetchall()]
|