112 lines
4.5 KiB
Python
112 lines
4.5 KiB
Python
"""Basic review write/read helpers separated from strategy-iteration logic."""
|
|
|
|
import json
|
|
from datetime import datetime
|
|
|
|
from app.db.schema import get_conn
|
|
|
|
|
|
def record_review(rec_id, symbol, outcome, pnl_48h, max_pnl_48h,
|
|
triggered_signals, hit_signals, miss_signals, lesson):
|
|
"""Insert one recommendation review row."""
|
|
conn = get_conn()
|
|
conn.execute("""
|
|
INSERT INTO review_log (rec_id, symbol, review_time, outcome, pnl_48h, max_pnl_48h,
|
|
triggered_signals, hit_signals, miss_signals, lesson)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
""", (
|
|
rec_id, symbol, datetime.now().isoformat(), outcome, pnl_48h, max_pnl_48h,
|
|
json.dumps(triggered_signals, ensure_ascii=False) if isinstance(triggered_signals, list) else triggered_signals,
|
|
json.dumps(hit_signals, ensure_ascii=False) if isinstance(hit_signals, list) else hit_signals,
|
|
json.dumps(miss_signals, ensure_ascii=False) if isinstance(miss_signals, list) else miss_signals,
|
|
lesson,
|
|
))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def update_signal_performance(signal_type, category, is_hit, pnl, weight_override=None):
|
|
"""Update rolling signal performance stats after review.
|
|
|
|
``weight_override`` is used by the daily review governance step to make
|
|
reviewed factor weights actually affect the next screening run.
|
|
"""
|
|
conn = get_conn()
|
|
row = conn.execute("SELECT * FROM signal_performance WHERE signal_type=%s", (signal_type,)).fetchone()
|
|
if weight_override is not None:
|
|
weight = max(0.0, float(weight_override or 0))
|
|
if row:
|
|
conn.execute(
|
|
"""
|
|
UPDATE signal_performance
|
|
SET weight=%s, last_updated=%s
|
|
WHERE signal_type=%s
|
|
""",
|
|
(weight, datetime.now().isoformat(), signal_type),
|
|
)
|
|
else:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO signal_performance (
|
|
signal_type, category, total_count, hit_count, miss_count,
|
|
hit_rate, avg_pnl, weight, last_updated
|
|
) VALUES (%s, %s, 0, 0, 0, 0, 0, %s, %s)
|
|
""",
|
|
(signal_type, category or "", weight, datetime.now().isoformat()),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
return
|
|
|
|
if row:
|
|
total = row["total_count"] + 1
|
|
hits = row["hit_count"] + (1 if is_hit else 0)
|
|
misses = row["miss_count"] + (0 if is_hit else 1)
|
|
old_avg_pnl = row["avg_pnl"]
|
|
new_avg_pnl = round((old_avg_pnl * (total - 1) + pnl) / total, 2)
|
|
hit_rate = round(hits / total * 100, 1) if total > 0 else 0
|
|
|
|
conn.execute("""
|
|
UPDATE signal_performance SET total_count=%s, hit_count=%s, miss_count=%s,
|
|
hit_rate=%s, avg_pnl=%s, weight=%s, last_updated=%s
|
|
WHERE signal_type=%s
|
|
""", (total, hits, misses, hit_rate, new_avg_pnl, hit_rate / 50, datetime.now().isoformat(), signal_type))
|
|
else:
|
|
conn.execute("""
|
|
INSERT INTO signal_performance (signal_type, category, total_count, hit_count, miss_count,
|
|
hit_rate, avg_pnl, weight, last_updated)
|
|
VALUES (%s, %s, 1, %s, %s, %s, %s, %s, %s)
|
|
""", (
|
|
signal_type, category, 1 if is_hit else 0, 0 if is_hit else 1,
|
|
100 if is_hit else 0, pnl, 2.0 if is_hit else 0, datetime.now().isoformat(),
|
|
))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def get_signal_weights():
|
|
"""Read dynamic signal weights."""
|
|
conn = get_conn()
|
|
rows = conn.execute("SELECT signal_type, category, weight, hit_rate, avg_pnl, total_count FROM signal_performance").fetchall()
|
|
conn.close()
|
|
return {row["signal_type"]: dict(row) for row in rows}
|
|
|
|
|
|
def record_missed_explosion(symbol, price_at_detect, price_before, gain_pct,
|
|
reason_missed, features_detected, lesson):
|
|
"""Insert one missed-explosion review row."""
|
|
conn = get_conn()
|
|
conn.execute("""
|
|
INSERT INTO missed_explosions (symbol, detect_time, price_at_detect, price_before,
|
|
gain_pct, reason_missed, features_detected, lesson)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
|
""", (
|
|
symbol, datetime.now().isoformat(), price_at_detect, price_before, gain_pct,
|
|
json.dumps(reason_missed, ensure_ascii=False) if isinstance(reason_missed, list) else reason_missed,
|
|
json.dumps(features_detected, ensure_ascii=False) if isinstance(features_detected, list) else features_detected,
|
|
lesson,
|
|
))
|
|
conn.commit()
|
|
conn.close()
|