6021a3883d
- 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>
118 lines
4.8 KiB
Python
118 lines
4.8 KiB
Python
"""
|
|
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()
|