""" One-time migration: local trumpsignal.db (SQLite) → Postgres. Called automatically by entrypoint.sh on first deploy. Uses DATABASE_URL env var (same one uvicorn uses), just strips +asyncpg. """ import os import sqlite3 import sys from datetime import datetime, timezone import psycopg2 import psycopg2.extras SQLITE_PATH = os.path.join(os.path.dirname(__file__), '..', 'trumpsignal.db') raw_url = os.environ.get('DATABASE_URL', '') if not raw_url: print("ERROR: DATABASE_URL not set") sys.exit(1) # asyncpg URL → psycopg2 URL PG_URL = raw_url.replace('postgresql+asyncpg://', 'postgresql://') def parse_dt(v): if v is None: return None if isinstance(v, (int, float)): return datetime.fromtimestamp(v, tz=timezone.utc).replace(tzinfo=None) for fmt in ('%Y-%m-%d %H:%M:%S.%f', '%Y-%m-%d %H:%M:%S'): try: return datetime.strptime(v, fmt) except ValueError: continue return v def migrate(): sq = sqlite3.connect(SQLITE_PATH) sq.row_factory = sqlite3.Row pg = psycopg2.connect(PG_URL) cur_sq = sq.cursor() cur_pg = pg.cursor() # ── posts ───────────────────────────────────────────────────────────────── cur_sq.execute("SELECT * FROM posts") posts = cur_sq.fetchall() psycopg2.extras.execute_values(cur_pg, """ INSERT INTO posts ( id, external_id, text, source, published_at, sentiment, ai_confidence, relevant, price_impact_asset, price_impact_m5, price_impact_m15, price_impact_m1h, price_at_post, signal, ai_reasoning, prefilter_reason, analysis_version, created_at ) VALUES %s ON CONFLICT (id) DO NOTHING """, [( r['id'], r['external_id'], r['text'], r['source'], parse_dt(r['published_at']), r['sentiment'], r['ai_confidence'], bool(r['relevant']), r['price_impact_asset'], r['price_impact_m5'], r['price_impact_m15'], r['price_impact_m1h'], r['price_at_post'], r['signal'], r['ai_reasoning'], r['prefilter_reason'], r['analysis_version'], parse_dt(r['created_at']), ) for r in posts]) pg.commit() print(f" ✓ {len(posts)} posts") # ── subscriptions ───────────────────────────────────────────────────────── cur_sq.execute("SELECT * FROM subscriptions") subs = cur_sq.fetchall() psycopg2.extras.execute_values(cur_pg, """ INSERT INTO subscriptions ( id, wallet_address, hl_api_key, active, subscribed_at, created_at, leverage, position_size_usd, take_profit_pct, stop_loss_pct, min_confidence ) VALUES %s ON CONFLICT (id) DO NOTHING """, [( r['id'], r['wallet_address'], r['hl_api_key'], bool(r['active']), parse_dt(r['subscribed_at']), parse_dt(r['created_at']), r['leverage'] if r['leverage'] is not None else 3, r['position_size_usd'] if r['position_size_usd'] is not None else 20.0, r['take_profit_pct'], r['stop_loss_pct'], r['min_confidence'] if r['min_confidence'] is not None else 80, ) for r in subs]) pg.commit() print(f" ✓ {len(subs)} subscriptions") # ── bot_trades ──────────────────────────────────────────────────────────── cur_sq.execute("SELECT * FROM bot_trades") trades = cur_sq.fetchall() psycopg2.extras.execute_values(cur_pg, """ INSERT INTO bot_trades ( id, asset, side, entry_price, exit_price, pnl_usd, hold_seconds, trigger_post_id, wallet_address, opened_at, closed_at, hl_order_id ) VALUES %s ON CONFLICT (id) DO NOTHING """, [( r['id'], r['asset'], r['side'], r['entry_price'], r['exit_price'], r['pnl_usd'], r['hold_seconds'], r['trigger_post_id'], r['wallet_address'], parse_dt(r['opened_at']), parse_dt(r['closed_at']), r['hl_order_id'], ) for r in trades]) pg.commit() print(f" ✓ {len(trades)} bot_trades") # ── reset sequences ─────────────────────────────────────────────────────── for table, col in [('posts', 'id'), ('subscriptions', 'id'), ('bot_trades', 'id')]: cur_pg.execute(f""" SELECT setval(pg_get_serial_sequence('{table}', '{col}'), COALESCE((SELECT MAX({col}) FROM {table}), 1)) """) pg.commit() sq.close() pg.close() print("✅ SQLite → Postgres migration complete") if __name__ == '__main__': migrate()