feat: auto-migrate SQLite data to Postgres on first deploy

- Add trumpsignal.db to git (one-time migration source)
- entrypoint.sh detects empty Postgres and runs migration automatically
- scripts/migrate_sqlite_to_postgres.py: posts, subscriptions, bot_trades
- Add psycopg2-binary to requirements for migration script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
k
2026-04-21 20:48:05 +08:00
parent 1deb6a3918
commit 6021a3883d
6 changed files with 149 additions and 4 deletions
+117
View File
@@ -0,0 +1,117 @@
"""
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()