#!/usr/bin/env python3 """ V5.4 Strategy Factory DB Migration Script - Creates `strategies` table - Adds strategy_id + strategy_name_snapshot to paper_trades, signal_indicators - Inserts existing 3 strategies with fixed UUIDs - Backfills strategy_id + strategy_name_snapshot for all existing records """ import os import sys import psycopg2 from psycopg2.extras import execute_values PG_HOST = os.environ.get("PG_HOST", "10.106.0.3") PG_PASS = os.environ.get("PG_PASS", "arb_engine_2026") PG_USER = "arb" PG_DB = "arb_engine" # Fixed UUIDs for existing strategies (deterministic, easy to recognize) LEGACY_STRATEGY_MAP = { "v53": ("00000000-0000-0000-0000-000000000053", "V5.3 Standard"), "v53_middle": ("00000000-0000-0000-0000-000000000054", "V5.3 Middle"), "v53_fast": ("00000000-0000-0000-0000-000000000055", "V5.3 Fast"), } # Default config values per strategy (from strategy JSON files) LEGACY_CONFIGS = { "v53": { "symbol": "BTCUSDT", # multi-symbol, use BTC as representative "cvd_fast_window": "30m", "cvd_slow_window": "4h", "weight_direction": 55, "weight_env": 25, "weight_aux": 15, "weight_momentum": 5, "entry_score": 75, "sl_atr_multiplier": 1.0, "tp1_ratio": 0.75, "tp2_ratio": 1.5, "timeout_minutes": 60, "flip_threshold": 75, "status": "running", "initial_balance": 10000.0, }, "v53_middle": { "symbol": "BTCUSDT", "cvd_fast_window": "15m", "cvd_slow_window": "1h", "weight_direction": 55, "weight_env": 25, "weight_aux": 15, "weight_momentum": 5, "entry_score": 75, "sl_atr_multiplier": 1.0, "tp1_ratio": 0.75, "tp2_ratio": 1.5, "timeout_minutes": 60, "flip_threshold": 75, "status": "running", "initial_balance": 10000.0, }, "v53_fast": { "symbol": "BTCUSDT", "cvd_fast_window": "5m", "cvd_slow_window": "30m", "weight_direction": 55, "weight_env": 25, "weight_aux": 15, "weight_momentum": 5, "entry_score": 75, "sl_atr_multiplier": 1.0, "tp1_ratio": 0.75, "tp2_ratio": 1.5, "timeout_minutes": 60, "flip_threshold": 75, "status": "running", "initial_balance": 10000.0, }, } def get_conn(): return psycopg2.connect( host=PG_HOST, user=PG_USER, password=PG_PASS, dbname=PG_DB ) def step1_create_strategies_table(cur): print("[Step 1] Creating strategies table...") cur.execute(""" CREATE TABLE IF NOT EXISTS strategies ( strategy_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), display_name TEXT NOT NULL, schema_version INT NOT NULL DEFAULT 1, status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'paused', 'deprecated')), status_changed_at TIMESTAMP, last_run_at TIMESTAMP, deprecated_at TIMESTAMP, symbol TEXT NOT NULL CHECK (symbol IN ('BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'XRPUSDT')), direction TEXT NOT NULL DEFAULT 'both' CHECK (direction IN ('long_only', 'short_only', 'both')), cvd_fast_window TEXT NOT NULL DEFAULT '30m' CHECK (cvd_fast_window IN ('5m', '15m', '30m')), cvd_slow_window TEXT NOT NULL DEFAULT '4h' CHECK (cvd_slow_window IN ('30m', '1h', '4h')), weight_direction INT NOT NULL DEFAULT 55, weight_env INT NOT NULL DEFAULT 25, weight_aux INT NOT NULL DEFAULT 15, weight_momentum INT NOT NULL DEFAULT 5, entry_score INT NOT NULL DEFAULT 75, gate_obi_enabled BOOL NOT NULL DEFAULT TRUE, obi_threshold FLOAT NOT NULL DEFAULT 0.3, gate_whale_enabled BOOL NOT NULL DEFAULT TRUE, whale_cvd_threshold FLOAT NOT NULL DEFAULT 0.0, gate_vol_enabled BOOL NOT NULL DEFAULT TRUE, atr_percentile_min INT NOT NULL DEFAULT 20, gate_spot_perp_enabled BOOL NOT NULL DEFAULT FALSE, spot_perp_threshold FLOAT NOT NULL DEFAULT 0.002, sl_atr_multiplier FLOAT NOT NULL DEFAULT 1.5, tp1_ratio FLOAT NOT NULL DEFAULT 0.75, tp2_ratio FLOAT NOT NULL DEFAULT 1.5, timeout_minutes INT NOT NULL DEFAULT 240, flip_threshold INT NOT NULL DEFAULT 80, initial_balance FLOAT NOT NULL DEFAULT 10000.0, current_balance FLOAT NOT NULL DEFAULT 10000.0, description TEXT, tags TEXT[], created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW() ) """) cur.execute("CREATE INDEX IF NOT EXISTS idx_strategies_status ON strategies(status)") cur.execute("CREATE INDEX IF NOT EXISTS idx_strategies_symbol ON strategies(symbol)") cur.execute("CREATE INDEX IF NOT EXISTS idx_strategies_last_run ON strategies(last_run_at)") print("[Step 1] Done.") def step2_add_columns(cur): print("[Step 2] Adding strategy_id + strategy_name_snapshot columns...") # paper_trades for col, col_type in [ ("strategy_id", "UUID REFERENCES strategies(strategy_id)"), ("strategy_name_snapshot", "TEXT"), ]: cur.execute(f""" ALTER TABLE paper_trades ADD COLUMN IF NOT EXISTS {col} {col_type} """) # signal_indicators for col, col_type in [ ("strategy_id", "UUID REFERENCES strategies(strategy_id)"), ("strategy_name_snapshot", "TEXT"), ]: cur.execute(f""" ALTER TABLE signal_indicators ADD COLUMN IF NOT EXISTS {col} {col_type} """) # Indexes cur.execute("CREATE INDEX IF NOT EXISTS idx_paper_trades_strategy_id ON paper_trades(strategy_id)") cur.execute("CREATE INDEX IF NOT EXISTS idx_si_strategy_id ON signal_indicators(strategy_id)") print("[Step 2] Done.") def step3_insert_legacy_strategies(cur): print("[Step 3] Inserting legacy strategies into strategies table...") for strategy_name, (uuid, display_name) in LEGACY_STRATEGY_MAP.items(): cfg = LEGACY_CONFIGS[strategy_name] # Compute current_balance from actual paper trades cur.execute(""" SELECT COALESCE(SUM(pnl_r) * 200, 0) as total_pnl_usdt FROM paper_trades WHERE strategy = %s AND status != 'active' """, (strategy_name,)) row = cur.fetchone() pnl_usdt = row[0] if row else 0 current_balance = round(cfg["initial_balance"] + pnl_usdt, 2) cur.execute(""" INSERT INTO strategies ( strategy_id, display_name, schema_version, status, symbol, direction, cvd_fast_window, cvd_slow_window, weight_direction, weight_env, weight_aux, weight_momentum, entry_score, gate_obi_enabled, obi_threshold, gate_whale_enabled, whale_cvd_threshold, gate_vol_enabled, atr_percentile_min, gate_spot_perp_enabled, spot_perp_threshold, sl_atr_multiplier, tp1_ratio, tp2_ratio, timeout_minutes, flip_threshold, initial_balance, current_balance, description ) VALUES ( %s, %s, 1, %s, %s, 'both', %s, %s, %s, %s, %s, %s, %s, TRUE, 0.3, TRUE, 0.0, TRUE, 20, FALSE, 0.002, %s, %s, %s, %s, %s, %s, %s, %s ) ON CONFLICT (strategy_id) DO NOTHING """, ( uuid, display_name, cfg["status"], cfg["symbol"], cfg["cvd_fast_window"], cfg["cvd_slow_window"], cfg["weight_direction"], cfg["weight_env"], cfg["weight_aux"], cfg["weight_momentum"], cfg["entry_score"], cfg["sl_atr_multiplier"], cfg["tp1_ratio"], cfg["tp2_ratio"], cfg["timeout_minutes"], cfg["flip_threshold"], cfg["initial_balance"], current_balance, f"Migrated from V5.3 legacy strategy: {strategy_name}" )) print(f" Inserted {strategy_name} → {uuid} (balance: {current_balance})") print("[Step 3] Done.") def step4_backfill(cur): print("[Step 4] Backfilling strategy_id + strategy_name_snapshot...") for strategy_name, (uuid, display_name) in LEGACY_STRATEGY_MAP.items(): # paper_trades cur.execute(""" UPDATE paper_trades SET strategy_id = %s::uuid, strategy_name_snapshot = %s WHERE strategy = %s AND strategy_id IS NULL """, (uuid, display_name, strategy_name)) count = cur.rowcount print(f" paper_trades [{strategy_name}]: {count} rows updated") # signal_indicators cur.execute(""" UPDATE signal_indicators SET strategy_id = %s::uuid, strategy_name_snapshot = %s WHERE strategy = %s AND strategy_id IS NULL """, (uuid, display_name, strategy_name)) count = cur.rowcount print(f" signal_indicators [{strategy_name}]: {count} rows updated") print("[Step 4] Done.") def step5_verify(cur): print("[Step 5] Verifying migration completeness...") # Check strategies table cur.execute("SELECT COUNT(*) FROM strategies") n = cur.fetchone()[0] print(f" strategies table: {n} rows") # Check NULL strategy_id in paper_trades (for known strategies) cur.execute(""" SELECT strategy, COUNT(*) as cnt FROM paper_trades WHERE strategy IN ('v53', 'v53_middle', 'v53_fast') AND strategy_id IS NULL GROUP BY strategy """) rows = cur.fetchall() if rows: print(f" WARNING: NULL strategy_id found in paper_trades:") for r in rows: print(f" {r[0]}: {r[1]} rows") else: print(" paper_trades: all known strategies backfilled ✅") # Check NULL in signal_indicators cur.execute(""" SELECT strategy, COUNT(*) as cnt FROM signal_indicators WHERE strategy IN ('v53', 'v53_middle', 'v53_fast') AND strategy_id IS NULL GROUP BY strategy """) rows = cur.fetchall() if rows: print(f" WARNING: NULL strategy_id found in signal_indicators:") for r in rows: print(f" {r[0]}: {r[1]} rows") else: print(" signal_indicators: all known strategies backfilled ✅") print("[Step 5] Done.") def main(): dry_run = "--dry-run" in sys.argv if dry_run: print("=== DRY RUN MODE (no changes will be committed) ===") conn = get_conn() conn.autocommit = False cur = conn.cursor() try: step1_create_strategies_table(cur) step2_add_columns(cur) step3_insert_legacy_strategies(cur) step4_backfill(cur) step5_verify(cur) if dry_run: conn.rollback() print("\n=== DRY RUN: rolled back all changes ===") else: conn.commit() print("\n=== Migration completed successfully ✅ ===") except Exception as e: conn.rollback() print(f"\n=== ERROR: {e} ===") raise finally: cur.close() conn.close() if __name__ == "__main__": main()