"""Price cache and recommendation tracking writes.""" from datetime import datetime from app.core.opportunity_lifecycle import is_executed_lifecycle from app.db.recommendation_state import derive_minimal_state_fields from app.db.schema import get_conn def update_recommendation_tracking(rec_id, current_price): """Update recommendation price/PnL and terminal state for executed positions.""" conn = get_conn() row = conn.execute(""" SELECT entry_price, max_price, min_price, symbol, status, action_status, execution_status, display_bucket, entry_triggered FROM recommendation WHERE id=%s """, (rec_id,)).fetchone() if not row: conn.close() return entry_price = row["entry_price"] old_max = row["max_price"] or entry_price old_min = row["min_price"] or entry_price new_max = max(old_max, current_price) new_min = min(old_min, current_price) pnl_pct = round((current_price / entry_price - 1) * 100, 2) max_pnl_pct = round((new_max / entry_price - 1) * 100, 2) max_drawdown_pct = round((new_min / entry_price - 1) * 100, 2) is_executed = ( int(row["entry_triggered"] or 0) == 1 or row["display_bucket"] == "position" or row["execution_status"] in ("holding", "completed") or is_executed_lifecycle(row["status"], row["action_status"], row["execution_status"]) ) status = "active" tp1_reached = False rec = conn.execute("SELECT stop_loss, tp1, tp2, status, hit_tp1_time FROM recommendation WHERE id=%s", (rec_id,)).fetchone() if rec and rec["status"] == "active" and is_executed: if rec["tp2"] and current_price >= rec["tp2"]: status = "hit_tp2" elif rec["tp1"] and current_price >= rec["tp1"]: status = "hit_tp1" tp1_reached = True elif rec["tp1"] == 0 and pnl_pct >= 15: status = "hit_tp1" tp1_reached = True elif rec["stop_loss"] and current_price <= rec["stop_loss"]: status = "stopped_out" now = datetime.now().isoformat() if status != "active": action_for_status = {"hit_tp1": "止盈1", "hit_tp2": "止盈2", "stopped_out": "止损"}.get(status, "持有") execution_status, display_bucket, lifecycle_state, entry_triggered, state_reason = derive_minimal_state_fields(status, action_for_status, {}) conn.execute(""" UPDATE recommendation SET current_price=%s, max_price=%s, min_price=%s, pnl_pct=%s, max_pnl_pct=%s, max_drawdown_pct=%s, status=%s, action_status=%s, execution_status=%s, display_bucket=%s, lifecycle_state=%s, entry_triggered=%s, state_reason=%s, last_track_time=%s, hit_tp1_time=CASE WHEN %s='hit_tp1' THEN %s ELSE hit_tp1_time END, hit_tp2_time=CASE WHEN %s='hit_tp2' THEN %s ELSE hit_tp2_time END, stopped_out_time=CASE WHEN %s='stopped_out' THEN %s ELSE stopped_out_time END WHERE id=%s """, ( current_price, new_max, new_min, pnl_pct, max_pnl_pct, max_drawdown_pct, status, action_for_status, execution_status, display_bucket, lifecycle_state, entry_triggered, state_reason, now, status, now, status, now, status, now, rec_id, )) else: conn.execute(""" UPDATE recommendation SET current_price=%s, max_price=%s, min_price=%s, pnl_pct=%s, max_pnl_pct=%s, max_drawdown_pct=%s, last_track_time=%s, hit_tp1_time=CASE WHEN %s=1 THEN COALESCE(NULLIF(hit_tp1_time,''), %s) ELSE hit_tp1_time END WHERE id=%s """, ( current_price, new_max, new_min, pnl_pct, max_pnl_pct, max_drawdown_pct, now, 1 if tp1_reached else 0, now, rec_id, )) symbol = row["symbol"] update_latest_price_cache(symbol, current_price, updated_at=now, source="tracker", conn=conn) conn.execute(""" INSERT INTO price_tracking (rec_id, symbol, track_time, price, pnl_pct) VALUES (%s, %s, %s, %s, %s) """, (rec_id, symbol, now, current_price, pnl_pct)) conn.commit() conn.close() return { "status": status, "tp1_reached": tp1_reached, "pnl_pct": pnl_pct, "max_pnl_pct": max_pnl_pct, "max_drawdown_pct": max_drawdown_pct, } def update_latest_price_cache(symbol, price, updated_at=None, source="tracker", conn=None): """Upsert latest ticker cache. Dashboards read this instead of price_tracking.""" symbol = str(symbol or "").strip().upper() try: price = float(price or 0) except Exception: price = 0 if not symbol or price <= 0: return False updated_at = updated_at or datetime.now().isoformat() owns_conn = conn is None if owns_conn: conn = get_conn() conn.execute(""" INSERT INTO latest_price_cache (symbol, price, updated_at, source) VALUES (%s, %s, %s, %s) ON CONFLICT(symbol) DO UPDATE SET price=excluded.price, updated_at=excluded.updated_at, source=excluded.source """, (symbol, price, updated_at, source)) if owns_conn: conn.commit() conn.close() return True def get_latest_price_cache(symbols): """Batch read latest price cache as {symbol: {price, updated_at, source}}.""" normalized = [] for sym in symbols or []: sym = str(sym or "").strip().upper() if sym and sym not in normalized: normalized.append(sym) if not normalized: return {} conn = get_conn() placeholders = ",".join(["%s"] * len(normalized)) rows = conn.execute( f"SELECT symbol, price, updated_at, source FROM latest_price_cache WHERE symbol IN ({placeholders})", tuple(normalized), ).fetchall() conn.close() return {row["symbol"]: dict(row) for row in rows} def latest_tracking_price(rec_id, fallback=0): """Compatibility shim: current price comes from latest_price_cache/recommendation.""" return fallback or 0 def update_entry_timing(rec_id: int, entry_price: float, rec_time: str): """Update triggered entry time/price when tracker promotes a setup to buy_now.""" conn = get_conn() conn.execute( "UPDATE recommendation SET rec_time=%s, entry_price=%s, current_price=%s, pnl_pct=0 WHERE id=%s", (rec_time, entry_price, entry_price, rec_id), ) conn.commit() conn.close()