161 lines
6.3 KiB
Python
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()
|