stock-prediction-api / scripts /db_diagnostics.py
Hermes Agent
feat: scenario simulator, risk profile widget, interactive FAQ accordion, and database diagnostics script
0378eb0
Raw
History Blame Contribute Delete
8.38 kB
import asyncio
import sys
import os
import time
from pathlib import Path
from dotenv import load_dotenv
# Add parent directory to sys.path and load dotenv safely
sys.path.insert(0, str(Path(__file__).parent.parent))
from app.services.database_service import DatabaseService
dotenv_path = Path(__file__).parent.parent / ".env"
load_dotenv(dotenv_path=dotenv_path)
async def run_diagnostics():
url = os.getenv("TURSO_URL")
token = os.getenv("TURSO_TOKEN")
if not url or not token:
print("Error: TURSO_URL and TURSO_TOKEN environment variables must be set in .env")
sys.exit(1)
db = DatabaseService(url=url, token=token)
await db.initialize()
try:
print("=" * 60)
print("DATABASE DIAGNOSTICS REPORT")
print("=" * 60)
# 1. Stock Coverage Breakdown
print("\n[1] Stock Coverage Breakdown")
coverage_sql = """
SELECT
s.country,
COUNT(CASE WHEN s.trading_status = 'active' THEN 1 END) as total_active,
COUNT(CASE WHEN s.trading_status = 'inactive' THEN 1 END) as total_inactive,
COUNT(DISTINCT CASE WHEN s.trading_status = 'active' AND f.symbol IS NOT NULL THEN s.yf_symbol END) as forecasted_active
FROM (
SELECT
country,
COALESCE(trading_status, 'active') as trading_status,
TRIM(REPLACE(symbol, 'NZX:', '')) || COALESCE(yfinance_suffix, '') as yf_symbol
FROM stocks
) s
LEFT JOIN (
SELECT DISTINCT symbol FROM forecasts
) f ON s.yf_symbol = f.symbol
GROUP BY s.country
ORDER BY s.country
"""
rows = await db._execute(coverage_sql)
print(f"{'Country':<10} | {'Active':<8} | {'Inactive':<8} | {'Forecasted':<10} | {'Coverage':<8}")
print("-" * 60)
for r in rows:
active = r['total_active']
forecasted = r['forecasted_active']
pct = (forecasted / active * 100) if active > 0 else 0.0
print(f"{r['country'].upper():<10} | {active:<8} | {r['total_inactive']:<8} | {forecasted:<10} | {pct:.2f}%")
# 2. Freshness & Staleness
print("\n[2] Forecast Freshness & Staleness (Last 24 Hours)")
now = int(time.time())
threshold = now - (24 * 3600)
freshness_sql = """
SELECT
COUNT(CASE WHEN created_at >= ? THEN 1 END) as fresh_count,
COUNT(CASE WHEN created_at < ? THEN 1 END) as stale_count
FROM forecasts
"""
fresh_rows = await db._execute(freshness_sql, [threshold, threshold])
print(f"Fresh Forecasts (Last 24h): {fresh_rows[0]['fresh_count']}")
print(f"Stale Forecasts (> 24h): {fresh_rows[0]['stale_count']}")
print("\n[2b] 5 Oldest Forecasts in Database")
oldest_sql = """
SELECT symbol, horizon_days, created_at
FROM forecasts
ORDER BY created_at ASC
LIMIT 5
"""
old_rows = await db._execute(oldest_sql)
for idx, r in enumerate(old_rows, 1):
created_str = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(r['created_at']))
print(f" {idx}. {r['symbol']:<10} ({r['horizon_days']}D) - Created: {created_str} UTC")
# 3. High Market Cap Missing Forecasts
print("\n[3] Top 5 Missing Forecasts per Country (by Market Cap)")
countries_res = await db._execute("SELECT DISTINCT country FROM stocks ORDER BY country")
countries = [r['country'] for r in countries_res if r['country']]
for country in countries:
missing_sql = """
SELECT s.symbol, s.name, s.market_cap
FROM stocks s
LEFT JOIN (
SELECT DISTINCT symbol FROM forecasts
) f ON (TRIM(REPLACE(s.symbol, 'NZX:', '')) || COALESCE(s.yfinance_suffix, '')) = f.symbol
WHERE s.country = ?
AND (s.trading_status = 'active' OR s.trading_status IS NULL)
AND (s.yfinance_available = 1 OR s.yfinance_available IS NULL)
AND f.symbol IS NULL
ORDER BY s.market_cap DESC NULLS LAST
LIMIT 5
"""
m_rows = await db._execute(missing_sql, [country])
print(f" Country: {country.upper()}")
for r in m_rows:
mcap_str = f"${r['market_cap'] / 1e9:.2f}B" if r['market_cap'] else "N/A"
print(f" - {r['symbol']:<8} | {r['name'][:30]:<30} | Market Cap: {mcap_str}")
# 4. Pipeline Integrity - Mismatched Horizons
print("\n[4] Pipeline Integrity: Mismatched Horizon Counts (< 4)")
mismatched_sql = """
SELECT symbol, COUNT(DISTINCT horizon_days) as horizon_count, GROUP_CONCAT(horizon_days) as horizons
FROM forecasts
GROUP BY symbol
HAVING horizon_count < 4
"""
mismatched_rows = await db._execute(mismatched_sql)
if mismatched_rows:
# Map back to stock details in Python to prevent slow SQL joins
sym_map = {}
for row in mismatched_rows[:10]:
yf_sym = row['symbol']
base = yf_sym
for suff in [".L", ".TO", ".AX", ".NZ", ".IR"]:
if yf_sym.endswith(suff):
base = yf_sym[:-len(suff)]
break
sym_map[base] = row
placeholders = ", ".join(["?"] * len(sym_map))
stocks_sql = f"SELECT symbol, country FROM stocks WHERE symbol IN ({placeholders})"
stock_rows = await db._execute(stocks_sql, list(sym_map.keys()))
stock_info = {r['symbol']: r for r in stock_rows}
print(f" Found {len(mismatched_rows)} symbols with mismatched horizons (showing top 10):")
for row in mismatched_rows[:10]:
yf_sym = row['symbol']
base = yf_sym
for suff in [".L", ".TO", ".AX", ".NZ", ".IR"]:
if yf_sym.endswith(suff):
base = yf_sym[:-len(suff)]
break
country = stock_info.get(base, {}).get('country', 'us').upper()
print(f" - {yf_sym:<10} ({country}) | Horizons present: {row['horizons']}")
else:
print(" No mismatched horizon counts found. All forecasted stocks have exactly 4 horizons.")
# 5. Pipeline Integrity - Out of Sync Horizons
print("\n[5] Pipeline Integrity: Out of Sync Horizons (> 60s difference)")
sync_sql = """
SELECT symbol, (MAX(created_at) - MIN(created_at)) as time_diff
FROM forecasts
GROUP BY symbol
HAVING time_diff > 60
"""
sync_rows = await db._execute(sync_sql)
if sync_rows:
sym_map = {}
for row in sync_rows[:10]:
yf_sym = row['symbol']
base = yf_sym
for suff in [".L", ".TO", ".AX", ".NZ", ".IR"]:
if yf_sym.endswith(suff):
base = yf_sym[:-len(suff)]
break
sym_map[base] = row
placeholders = ", ".join(["?"] * len(sym_map))
stocks_sql = f"SELECT symbol, country FROM stocks WHERE symbol IN ({placeholders})"
stock_rows = await db._execute(stocks_sql, list(sym_map.keys()))
stock_info = {r['symbol']: r for r in stock_rows}
print(f" Found {len(sync_rows)} symbols out of sync (showing top 10):")
for row in sync_rows[:10]:
yf_sym = row['symbol']
base = yf_sym
for suff in [".L", ".TO", ".AX", ".NZ", ".IR"]:
if yf_sym.endswith(suff):
base = yf_sym[:-len(suff)]
break
country = stock_info.get(base, {}).get('country', 'us').upper()
print(f" - {yf_sym:<10} ({country}) | Max creation delay: {row['time_diff']}s")
else:
print(" No out of sync horizons found. All forecasts generated within 60 seconds of each other.")
print("=" * 60)
finally:
await db.close()
if __name__ == "__main__":
asyncio.run(run_diagnostics())