#!/usr/bin/env python3
"""
public_paper_trader.py — Paper Trading Script for Public.com
============================================================
Simulates swing trades on the Public.com watchlist using:
  - RSI(14), MACD(12,26,9), EMA(20), SMA(50), volume ratio
  - 5-min bars via yfinance (live market hours) or daily bars (after-hours/backtest)
  - State persisted to JSON
  - End-of-day Signal DM summary to Ghost

Public.com API Integration:
  - Base URL: https://api.public.com/v1
  - Auth: OAuth 2.0 Bearer token (set PUBLIC_API_TOKEN env var)
  - Endpoints used (when live creds available):
      GET  /accounts                  -> account list
      GET  /accounts/{id}/portfolio   -> positions + balance
      POST /orders                    -> place order
      GET  /orders                    -> order history
      GET  /instruments?symbol=AAPL   -> instrument details + real-time quote
  - All endpoints require: Authorization: Bearer <token>
  - In paper mode (default): API calls are MOCKED; no real orders placed

Usage:
  python3 public_paper_trader.py                   # run paper trading cycle
  python3 public_paper_trader.py --eod-summary     # force end-of-day summary + Signal DM
  python3 public_paper_trader.py --reset           # wipe state file and start fresh
  python3 public_paper_trader.py --status          # print current portfolio
  PUBLIC_API_TOKEN=xxx python3 public_paper_trader.py --live-quotes  # use real quotes

DO NOT set --place-real-orders without explicit review. Paper mode is the default and only
supported mode in this script. Real order placement requires uncommenting a guarded block.
"""

import os
import sys
import json
import argparse
import datetime
import subprocess
from pathlib import Path

import pandas as pd
import yfinance as yf

try:
    import ta
    HAS_TA = True
except ImportError:
    HAS_TA = False
    print("[WARN] 'ta' library not found; using manual indicator calculations")

# ─── Configuration ──────────────────────────────────────────────────────────

WATCHLIST = ["AAPL", "MSFT", "NVDA", "GOOGL", "TSLA", "SPY", "QQQ", "MSTR", "WDAY", "CRM", "ADBE"]

STATE_FILE = Path(__file__).parent / "paper_state.json"
LOG_FILE   = Path(__file__).parent / "paper_trades.jsonl"

PORTFOLIO_START = 10_000.0   # paper starting cash ($)

# Safety Rails (from project spec)
STOP_LOSS_PCT        = 0.03   # 3% per-trade stop
DAILY_LOSS_LIMIT_PCT = 0.02   # 2% daily loss halts new entries
MAX_POSITION_SIZE    = 0.15   # 15% of portfolio per trade
MAX_POSITIONS        = 4      # max concurrent open positions
MIN_GROSS_TARGET     = 0.018  # 1.8% minimum profit target
HOLD_DAYS_MAX        = 30     # close after 30 days if no signal

# Signal thresholds
RSI_OVERSOLD         = 35     # buy signal when RSI crosses up from below
RSI_OVERBOUGHT       = 65     # sell signal when RSI crosses above
SIGNAL_STRENGTH_MIN  = 2      # number of agreeing signals to trigger entry

# Public.com API (paper mode by default)
PUBLIC_API_BASE  = "https://api.public.com/v1"
PUBLIC_API_TOKEN = os.environ.get("PUBLIC_API_TOKEN", "")
USE_LIVE_QUOTES  = bool(PUBLIC_API_TOKEN) and "--live-quotes" in sys.argv

# Signal DM config
SIGNAL_RECIPIENT = "+15406208059"

# ─── State Management ───────────────────────────────────────────────────────

def load_state() -> dict:
    if STATE_FILE.exists():
        with open(STATE_FILE) as f:
            return json.load(f)
    return {
        "cash": PORTFOLIO_START,
        "portfolio_start": PORTFOLIO_START,
        "cycle_start": PORTFOLIO_START,
        "safe_reserve": 0.0,
        "positions": {},         # symbol -> {shares, entry_price, entry_date, stop_loss}
        "daily_loss": 0.0,
        "daily_loss_date": None,
        "trades_today": [],
        "total_trades": 0,
        "total_wins": 0,
        "total_losses": 0,
        "halted": False,
        "halt_reason": "",
        "created": datetime.datetime.now().isoformat(),
        "last_run": None,
    }

def save_state(state: dict):
    STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
    with open(STATE_FILE, "w") as f:
        json.dump(state, f, indent=2, default=str)

def log_trade(trade: dict):
    LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
    with open(LOG_FILE, "a") as f:
        f.write(json.dumps(trade, default=str) + "\n")

# ─── Public.com API Client (Mock + Real) ────────────────────────────────────

class PublicAPIClient:
    """
    Thin wrapper around the Public.com REST API.
    In paper mode (no token), all write calls are mocked.
    Read calls (quotes) fall back to yfinance.

    Auth flow:
      1. User generates OAuth 2.0 token at app.public.com/settings/api
      2. Set PUBLIC_API_TOKEN env var
      3. All requests use: Authorization: Bearer <token>

    Key endpoints:
      GET  /accounts                         List accounts
      GET  /accounts/{id}/portfolio          Portfolio summary
      GET  /instruments?symbol={sym}         Instrument + real-time quote
      POST /orders                           Place order
        body: {account_id, symbol, side, type, quantity, limit_price, time_in_force}
      GET  /orders?account_id={id}           Order list
    """

    def __init__(self, token: str = "", paper: bool = True):
        self.token = token
        self.paper = paper or not token

    def _headers(self) -> dict:
        return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}

    def get_quote(self, symbol: str) -> float:
        """Get current price. Uses Public API if token available, else yfinance."""
        if self.token and USE_LIVE_QUOTES:
            try:
                import requests
                r = requests.get(
                    f"{PUBLIC_API_BASE}/instruments",
                    params={"symbol": symbol},
                    headers=self._headers(),
                    timeout=5
                )
                r.raise_for_status()
                data = r.json()
                return float(data.get("last_price") or data.get("price", 0))
            except Exception as e:
                print(f"[WARN] Public API quote failed ({e}), falling back to yfinance")
        # Fallback: yfinance
        try:
            ticker = yf.Ticker(symbol)
            hist = ticker.history(period="1d", interval="1m")
            if not hist.empty:
                return float(hist["Close"].iloc[-1])
        except Exception:
            pass
        return 0.0

    def place_order(self, account_id: str, symbol: str, side: str, quantity: float,
                    order_type: str = "limit", limit_price: float = None) -> dict:
        """
        Place an order. In paper mode, logs and returns a mock response.
        Real orders require uncommenting the guarded block below.
        """
        order = {
            "account_id": account_id,
            "symbol": symbol,
            "side": side,          # "buy" or "sell"
            "type": order_type,    # "market", "limit", "stop", "stop_limit"
            "quantity": quantity,
            "limit_price": limit_price,
            "time_in_force": "day",
        }
        if self.paper:
            mock_id = f"PAPER-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}-{symbol}"
            print(f"  [PAPER] {side.upper()} {quantity:.4f} {symbol} @ ${limit_price or 'market'}")
            return {"order_id": mock_id, "status": "filled", "filled_price": limit_price, **order}

        # ── REAL ORDER GUARD ─────────────────────────────────────────────────
        # Uncommenting this block places REAL orders. DO NOT enable without:
        #   1. Ghost's explicit approval
        #   2. Valid PUBLIC_API_TOKEN with write scope
        #   3. Paper trading gate passed (positive expectancy confirmed)
        # ─────────────────────────────────────────────────────────────────────
        # import requests
        # r = requests.post(f"{PUBLIC_API_BASE}/orders", json=order, headers=self._headers(), timeout=10)
        # r.raise_for_status()
        # return r.json()
        raise RuntimeError("Real order placement is disabled. Set paper=True or enable the guard block.")

# ─── Indicators ─────────────────────────────────────────────────────────────

def fetch_ohlcv(symbol: str, period: str = "60d", interval: str = "1d") -> pd.DataFrame:
    """Fetch OHLCV data. Uses 5-min bars during market hours for signal generation,
    daily bars for swing trade context."""
    try:
        df = yf.download(symbol, period=period, interval=interval,
                         progress=False, auto_adjust=True)
        df.columns = [c[0] if isinstance(c, tuple) else c for c in df.columns]
        return df.dropna()
    except Exception as e:
        print(f"[ERROR] Could not fetch data for {symbol}: {e}")
        return pd.DataFrame()

def compute_indicators(df: pd.DataFrame) -> pd.DataFrame:
    """Compute RSI(14), MACD(12,26,9), EMA(20), SMA(50), volume ratio."""
    if df.empty or len(df) < 26:
        return df

    close = df["Close"].squeeze()
    volume = df["Volume"].squeeze()

    if HAS_TA:
        df["rsi"] = ta.momentum.RSIIndicator(close, window=14).rsi()
        macd_obj = ta.trend.MACD(close, window_slow=26, window_fast=12, window_sign=9)
        df["macd"]        = macd_obj.macd()
        df["macd_signal"] = macd_obj.macd_signal()
        df["macd_hist"]   = macd_obj.macd_diff()
        df["ema20"]       = ta.trend.EMAIndicator(close, window=20).ema_indicator()
        df["sma50"]       = ta.trend.SMAIndicator(close, window=50).sma_indicator()
    else:
        # Manual calculations (no ta lib)
        delta = close.diff()
        gain = delta.clip(lower=0).rolling(14).mean()
        loss = (-delta.clip(upper=0)).rolling(14).mean()
        rs = gain / loss.replace(0, float("nan"))
        df["rsi"] = 100 - (100 / (1 + rs))

        ema12 = close.ewm(span=12, adjust=False).mean()
        ema26 = close.ewm(span=26, adjust=False).mean()
        df["macd"]        = ema12 - ema26
        df["macd_signal"] = df["macd"].ewm(span=9, adjust=False).mean()
        df["macd_hist"]   = df["macd"] - df["macd_signal"]
        df["ema20"]       = close.ewm(span=20, adjust=False).mean()
        df["sma50"]       = close.rolling(50).mean()

    # Volume ratio: current vs 20-period average
    vol_avg = volume.rolling(20).mean()
    df["vol_ratio"] = volume / vol_avg.replace(0, float("nan"))

    return df

def compute_signal(df: pd.DataFrame) -> dict:
    """
    Compute signal_strength (0-5) and direction ('buy', 'sell', 'hold').
    Entry requires signal_strength >= SIGNAL_STRENGTH_MIN.

    Scoring (buy):
      +1  RSI < RSI_OVERSOLD (oversold)
      +1  MACD histogram turning positive (momentum shift)
      +1  Price > EMA20 (short-term uptrend)
      +1  EMA20 > SMA50 (medium-term uptrend)
      +1  Volume ratio > 1.5 (volume confirmation)

    Scoring (sell / exit):
      +1  RSI > RSI_OVERBOUGHT
      +1  MACD histogram turning negative
      +1  Price < EMA20
      +1  EMA20 < SMA50
      +1  Volume ratio > 1.5 on down day
    """
    result = {"direction": "hold", "signal_strength": 0, "details": {}}
    if df.empty or len(df) < 2:
        return result

    row  = df.iloc[-1]
    prev = df.iloc[-2]

    def safe(val):
        try:
            return float(val) if pd.notna(val) else None
        except Exception:
            return None

    rsi      = safe(row.get("rsi"))
    macd_h   = safe(row.get("macd_hist"))
    prev_mh  = safe(prev.get("macd_hist"))
    price    = safe(row.get("Close"))
    ema20    = safe(row.get("ema20"))
    sma50    = safe(row.get("sma50"))
    vol_r    = safe(row.get("vol_ratio"))
    prev_c   = safe(prev.get("Close"))

    buy_score = 0
    sell_score = 0

    if rsi is not None:
        if rsi < RSI_OVERSOLD:
            buy_score += 1
        if rsi > RSI_OVERBOUGHT:
            sell_score += 1

    if macd_h is not None and prev_mh is not None:
        if macd_h > 0 and prev_mh <= 0:   # bullish crossover
            buy_score += 1
        if macd_h < 0 and prev_mh >= 0:   # bearish crossover
            sell_score += 1
        elif macd_h > 0:
            buy_score += 0.5
        elif macd_h < 0:
            sell_score += 0.5

    if price is not None and ema20 is not None:
        if price > ema20:
            buy_score += 1
        else:
            sell_score += 1

    if ema20 is not None and sma50 is not None:
        if ema20 > sma50:
            buy_score += 1
        else:
            sell_score += 1

    if vol_r is not None and vol_r > 1.5:
        if price is not None and prev_c is not None:
            if price >= prev_c:
                buy_score += 1
            else:
                sell_score += 1

    result["details"] = {
        "rsi": round(rsi, 2) if rsi else None,
        "macd_hist": round(macd_h, 4) if macd_h else None,
        "price": price,
        "ema20": round(ema20, 2) if ema20 else None,
        "sma50": round(sma50, 2) if sma50 else None,
        "vol_ratio": round(vol_r, 2) if vol_r else None,
        "buy_score": buy_score,
        "sell_score": sell_score,
    }

    if buy_score > sell_score and buy_score >= SIGNAL_STRENGTH_MIN:
        result["direction"] = "buy"
        result["signal_strength"] = buy_score
    elif sell_score > buy_score and sell_score >= SIGNAL_STRENGTH_MIN:
        result["direction"] = "sell"
        result["signal_strength"] = sell_score
    else:
        result["direction"] = "hold"
        result["signal_strength"] = max(buy_score, sell_score)

    return result

# ─── Portfolio Logic ─────────────────────────────────────────────────────────

def portfolio_value(state: dict, prices: dict) -> float:
    val = state["cash"]
    for sym, pos in state["positions"].items():
        price = prices.get(sym, pos["entry_price"])
        val += pos["shares"] * price
    return val

def check_stop_losses(state: dict, prices: dict, client: PublicAPIClient) -> list:
    """Check and execute stop losses. Returns list of closed positions."""
    closed = []
    today = datetime.date.today().isoformat()

    for sym in list(state["positions"].keys()):
        pos = state["positions"][sym]
        price = prices.get(sym, 0)
        if price <= 0:
            continue

        stop = pos["stop_loss"]
        entry = pos["entry_price"]

        # Check stop-loss breach
        hit_stop = price <= stop

        # Check max hold duration
        entry_date = datetime.date.fromisoformat(pos["entry_date"][:10])
        days_held = (datetime.date.today() - entry_date).days
        hit_duration = days_held >= HOLD_DAYS_MAX

        if hit_stop or hit_duration:
            pnl = (price - entry) * pos["shares"]
            pct = (price - entry) / entry * 100

            trade_log = {
                "ts": datetime.datetime.now().isoformat(),
                "action": "SELL",
                "symbol": sym,
                "shares": pos["shares"],
                "entry_price": entry,
                "exit_price": price,
                "pnl": round(pnl, 2),
                "pct": round(pct, 2),
                "reason": "stop_loss" if hit_stop else "max_duration",
            }
            client.place_order("paper", sym, "sell", pos["shares"], "stop_limit", price)
            state["cash"] += pos["shares"] * price
            state["daily_loss"] += min(0, pnl)
            state["total_trades"] += 1
            if pnl >= 0:
                state["total_wins"] += 1
            else:
                state["total_losses"] += 1
            state["trades_today"].append(trade_log)
            log_trade(trade_log)
            del state["positions"][sym]
            closed.append(trade_log)

    return closed

def check_profit_targets(state: dict, prices: dict, client: PublicAPIClient) -> list:
    """Check RSI overbought exits for open positions."""
    closed = []
    for sym in list(state["positions"].keys()):
        pos = state["positions"][sym]
        price = prices.get(sym, 0)
        if price <= 0:
            continue
        pnl = (price - pos["entry_price"]) * pos["shares"]
        pct = (price - pos["entry_price"]) / pos["entry_price"]
        if pct >= MIN_GROSS_TARGET:
            df = fetch_ohlcv(sym, period="30d", interval="1d")
            df = compute_indicators(df)
            sig = compute_signal(df)
            if sig["direction"] == "sell":
                trade_log = {
                    "ts": datetime.datetime.now().isoformat(),
                    "action": "SELL",
                    "symbol": sym,
                    "shares": pos["shares"],
                    "entry_price": pos["entry_price"],
                    "exit_price": price,
                    "pnl": round(pnl, 2),
                    "pct": round(pct * 100, 2),
                    "reason": "sell_signal",
                    "signal_detail": sig["details"],
                }
                client.place_order("paper", sym, "sell", pos["shares"], "limit", price)
                state["cash"] += pos["shares"] * price
                state["total_trades"] += 1
                if pnl >= 0:
                    state["total_wins"] += 1
                else:
                    state["total_losses"] += 1
                state["trades_today"].append(trade_log)
                log_trade(trade_log)
                del state["positions"][sym]
                closed.append(trade_log)
    return closed

def try_entries(state: dict, prices: dict, client: PublicAPIClient) -> list:
    """Scan watchlist for buy signals and open positions."""
    opened = []

    # Daily loss limit check
    today = datetime.date.today().isoformat()
    if state.get("daily_loss_date") != today:
        state["daily_loss"] = 0.0
        state["daily_loss_date"] = today
        state["trades_today"] = []
        state["halted"] = False
        state["halt_reason"] = ""

    total_val = portfolio_value(state, prices)
    daily_loss_threshold = total_val * DAILY_LOSS_LIMIT_PCT

    if state.get("halted"):
        print(f"[HALTED] Trading halted: {state['halt_reason']}")
        return []

    if abs(state["daily_loss"]) >= daily_loss_threshold:
        state["halted"] = True
        state["halt_reason"] = f"Daily loss limit hit (${state['daily_loss']:.2f})"
        print(f"[HALT] {state['halt_reason']}")
        return []

    n_positions = len(state["positions"])
    if n_positions >= MAX_POSITIONS:
        print(f"[SKIP] Max positions ({MAX_POSITIONS}) reached")
        return []

    for sym in WATCHLIST:
        if sym in state["positions"]:
            continue
        if n_positions >= MAX_POSITIONS:
            break

        price = prices.get(sym, 0)
        if price <= 0:
            continue

        df = fetch_ohlcv(sym, period="60d", interval="1d")
        df = compute_indicators(df)
        sig = compute_signal(df)

        print(f"  {sym}: {sig['direction'].upper()} (strength={sig['signal_strength']:.1f}) "
              f"RSI={sig['details'].get('rsi','?')} "
              f"vol_ratio={sig['details'].get('vol_ratio','?')}")

        if sig["direction"] != "buy" or sig["signal_strength"] < SIGNAL_STRENGTH_MIN:
            continue

        # Position sizing
        max_spend = total_val * MAX_POSITION_SIZE
        spend = min(max_spend, state["cash"] * 0.95)
        if spend < 10:
            print(f"  [SKIP] Not enough cash for {sym}")
            continue

        shares = spend / price
        stop_loss = price * (1 - STOP_LOSS_PCT)

        trade_log = {
            "ts": datetime.datetime.now().isoformat(),
            "action": "BUY",
            "symbol": sym,
            "shares": round(shares, 4),
            "entry_price": price,
            "stop_loss": round(stop_loss, 2),
            "signal_strength": sig["signal_strength"],
            "signal_detail": sig["details"],
        }

        client.place_order("paper", sym, "buy", shares, "limit", price)
        state["cash"] -= shares * price
        state["positions"][sym] = {
            "shares": round(shares, 4),
            "entry_price": price,
            "entry_date": datetime.datetime.now().isoformat(),
            "stop_loss": round(stop_loss, 2),
        }
        state["total_trades"] += 1
        state["trades_today"].append(trade_log)
        log_trade(trade_log)
        opened.append(trade_log)
        n_positions += 1

    return opened

# ─── Cycle Check ────────────────────────────────────────────────────────────

def check_cycle(state: dict, total_val: float):
    """Check if cycle target (+15%) hit. Lock 50% profit to safe_reserve."""
    cycle_target = state["cycle_start"] * 1.15
    if total_val >= cycle_target:
        profit = total_val - state["cycle_start"]
        lock   = profit * 0.50
        state["safe_reserve"] += lock
        state["cycle_start"]   = total_val - lock
        msg = (f"🔄 CYCLE COMPLETE! Portfolio hit +15%. "
               f"Locking ${lock:.2f} to safe reserve (total reserve: ${state['safe_reserve']:.2f}).")
        print(msg)
        if state["safe_reserve"] >= 1000:
            print(f"⚠️  ALERT: Safe reserve >= $1,000. Manual withdrawal opportunity.")
        return msg
    return None

# ─── Reporting ───────────────────────────────────────────────────────────────

def build_eod_summary(state: dict, prices: dict) -> str:
    today = datetime.date.today().strftime("%Y-%m-%d")
    total_val = portfolio_value(state, prices)
    gain_total = total_val - state["portfolio_start"]
    gain_pct   = gain_total / state["portfolio_start"] * 100
    win_rate   = (state["total_wins"] / state["total_trades"] * 100
                  if state["total_trades"] > 0 else 0)

    lines = [
        f"📊 Paper Trader EOD — {today}",
        f"Portfolio: ${total_val:,.2f} ({gain_pct:+.2f}% all-time)",
        f"Cash: ${state['cash']:,.2f} | Safe Reserve: ${state['safe_reserve']:,.2f}",
        f"Trades: {state['total_trades']} total | Win rate: {win_rate:.0f}%",
        "",
        f"Open positions ({len(state['positions'])}/{MAX_POSITIONS}):",
    ]
    for sym, pos in state["positions"].items():
        price = prices.get(sym, pos["entry_price"])
        pnl = (price - pos["entry_price"]) * pos["shares"]
        pct = (price - pos["entry_price"]) / pos["entry_price"] * 100
        lines.append(f"  {sym}: {pos['shares']:.2f}sh @ ${pos['entry_price']:.2f} "
                     f"→ ${price:.2f} ({pct:+.1f}%) PnL: ${pnl:+.2f}")

    if state["trades_today"]:
        lines.append("")
        lines.append(f"Today's trades ({len(state['trades_today'])}):")
        for t in state["trades_today"]:
            if t["action"] == "BUY":
                lines.append(f"  BUY  {t['symbol']} {t['shares']:.2f}sh @ ${t['entry_price']:.2f}")
            else:
                lines.append(f"  SELL {t['symbol']} {t['shares']:.2f}sh | PnL: ${t.get('pnl',0):+.2f} ({t.get('pct',0):+.1f}%)")

    if state.get("halted"):
        lines.append(f"\n⚠️  HALTED: {state['halt_reason']}")

    return "\n".join(lines)

def send_signal_dm(message: str):
    """Send Signal DM via openclaw message tool (invoked via CLI subprocess)."""
    try:
        cmd = [
            "openclaw", "message", "send",
            "--channel", "signal",
            "--target", SIGNAL_RECIPIENT,
            "--message", message,
        ]
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        if result.returncode == 0:
            print("[OK] Signal DM sent")
        else:
            print(f"[WARN] Signal DM failed: {result.stderr}")
    except Exception as e:
        print(f"[WARN] Could not send Signal DM: {e}")
        print("[FALLBACK] EOD Summary:\n", message)

# ─── Main ────────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(description="Public.com Paper Trader")
    parser.add_argument("--eod-summary", action="store_true", help="Send EOD summary to Ghost")
    parser.add_argument("--reset",       action="store_true", help="Reset state file")
    parser.add_argument("--status",      action="store_true", help="Print portfolio status")
    parser.add_argument("--live-quotes", action="store_true", help="Use Public.com live quotes")
    parser.add_argument("--no-signal",   action="store_true", help="Skip Signal DM")
    args = parser.parse_args()

    if args.reset:
        if STATE_FILE.exists():
            STATE_FILE.unlink()
            print("[RESET] State file deleted.")
        return

    state = load_state()

    # Reset daily state if new day
    today = datetime.date.today().isoformat()
    if state.get("daily_loss_date") != today:
        state["daily_loss"] = 0.0
        state["daily_loss_date"] = today
        state["trades_today"] = []

    client = PublicAPIClient(token=PUBLIC_API_TOKEN, paper=True)

    print(f"\n{'='*60}")
    print(f"Public.com Paper Trader — {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}")
    print(f"Mode: PAPER | Live quotes: {USE_LIVE_QUOTES}")
    print(f"{'='*60}\n")

    # Fetch current prices
    print("Fetching prices...")
    prices = {}
    for sym in WATCHLIST:
        p = client.get_quote(sym)
        if p:
            prices[sym] = p
            print(f"  {sym}: ${p:.2f}")

    if not prices:
        print("[ERROR] Could not fetch any prices. Check internet connection.")
        return

    if args.status:
        total_val = portfolio_value(state, prices)
        print(f"\nPortfolio value: ${total_val:,.2f}")
        print(f"Cash: ${state['cash']:,.2f}")
        for sym, pos in state["positions"].items():
            p = prices.get(sym, pos["entry_price"])
            pct = (p - pos["entry_price"]) / pos["entry_price"] * 100
            print(f"  {sym}: {pos['shares']:.4f}sh @ ${pos['entry_price']:.2f} → ${p:.2f} ({pct:+.1f}%)")
        return

    # --- Trading cycle ---
    print("\n--- Checking stop losses ---")
    stopped = check_stop_losses(state, prices, client)
    if stopped:
        for t in stopped:
            print(f"  STOP: {t['symbol']} PnL=${t['pnl']:+.2f} ({t['pct']:+.1f}%) [{t['reason']}]")

    print("\n--- Checking profit targets ---")
    exited = check_profit_targets(state, prices, client)
    if exited:
        for t in exited:
            print(f"  EXIT: {t['symbol']} PnL=${t['pnl']:+.2f} ({t['pct']:+.1f}%)")

    print("\n--- Scanning for entries ---")
    opened = try_entries(state, prices, client)
    if opened:
        for t in opened:
            print(f"  ENTER: {t['symbol']} strength={t['signal_strength']:.1f}")

    # Cycle check
    total_val = portfolio_value(state, prices)
    cycle_msg = check_cycle(state, total_val)

    state["last_run"] = datetime.datetime.now().isoformat()
    save_state(state)

    # EOD summary
    if args.eod_summary:
        summary = build_eod_summary(state, prices)
        print(f"\n{'='*60}")
        print(summary)
        print(f"{'='*60}")
        if not args.no_signal:
            send_signal_dm(summary)

    print(f"\nDone. Portfolio: ${total_val:,.2f}")

if __name__ == "__main__":
    main()
