alphax/app/db/tracking_queries.py
2026-05-20 00:57:46 +08:00

161 lines
6.3 KiB
Python

"""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()