alphax/app/db/review_queries.py
2026-05-16 14:52:10 +08:00

320 lines
13 KiB
Python

"""Review and strategy iteration-facing DB API."""
import json
import re
from datetime import datetime, timedelta
from app.db.altcoin_db import (
_loads_json_field,
backfill_strategy_failure_patterns,
dry_run_strategy_candidate_performance,
generate_candidates_from_review_history,
get_strategy_failure_patterns,
get_strategy_insights,
get_strategy_iteration_dashboard,
get_strategy_rule_candidates,
refresh_strategy_candidate_performance,
)
from app.db.schema import get_conn
def log_strategy_iteration(
run_date=None,
trigger_source="daily_review",
title="",
summary="",
findings=None,
problems=None,
actions=None,
changed_rules=None,
metrics=None,
related_symbols=None,
config_diff=None,
effect_summary=None,
pollution_summary=None,
strategy_version="",
version_change_summary="",
success_analysis=None,
failure_analysis=None,
candidate_rules=None,
release_decision="",
release_reason="",
confidence_level="",
promotion_state="research_only",
conn_provider=None,
):
"""记录一次策略复盘/迭代日志"""
conn_factory = conn_provider or get_conn
conn = conn_factory()
now = datetime.now().isoformat()
run_date = run_date or now[:10]
conn.execute(
"""
INSERT INTO strategy_iteration_log (
run_date, created_at, trigger_source, title, summary,
findings_json, problems_json, actions_json, changed_rules_json,
metrics_json, related_symbols_json, config_diff_json, effect_summary_json,
pollution_summary_json,
strategy_version, version_change_summary,
success_analysis_json, failure_analysis_json, candidate_rules_json,
release_decision, release_reason, confidence_level, promotion_state
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(
run_date,
now,
trigger_source or "daily_review",
title or "未命名迭代",
summary or "",
json.dumps(findings or [], ensure_ascii=False, default=str),
json.dumps(problems or [], ensure_ascii=False, default=str),
json.dumps(actions or [], ensure_ascii=False, default=str),
json.dumps(changed_rules or [], ensure_ascii=False, default=str),
json.dumps(metrics or {}, ensure_ascii=False, default=str),
json.dumps(related_symbols or [], ensure_ascii=False, default=str),
json.dumps(config_diff or {}, ensure_ascii=False, default=str),
json.dumps(effect_summary or {}, ensure_ascii=False, default=str),
json.dumps(pollution_summary or {}, ensure_ascii=False, default=str),
(strategy_version or "").strip(),
(version_change_summary or "").strip(),
json.dumps(success_analysis or {}, ensure_ascii=False, default=str),
json.dumps(failure_analysis or {}, ensure_ascii=False, default=str),
json.dumps(candidate_rules or [], ensure_ascii=False, default=str),
(release_decision or "").strip(),
(release_reason or "").strip(),
(confidence_level or "").strip(),
(promotion_state or "research_only").strip(),
),
)
conn.commit()
conn.close()
def get_strategy_iteration_logs(limit=30, conn_provider=None, json_loader=None):
conn_factory = conn_provider or get_conn
loader = json_loader or _loads_json_field
conn = conn_factory()
rows = conn.execute(
"""
SELECT * FROM strategy_iteration_log
ORDER BY created_at DESC, id DESC
LIMIT %s
""",
(limit,),
).fetchall()
conn.close()
result = []
for row in rows:
item = dict(row)
item["findings"] = loader(item.get("findings_json"), [])
item["problems"] = loader(item.get("problems_json"), [])
item["actions"] = loader(item.get("actions_json"), [])
item["changed_rules"] = loader(item.get("changed_rules_json"), [])
item["metrics"] = loader(item.get("metrics_json"), {})
item["related_symbols"] = loader(item.get("related_symbols_json"), [])
item["config_diff"] = loader(item.get("config_diff_json"), {})
item["effect_summary"] = loader(item.get("effect_summary_json"), {})
item["pollution_summary"] = loader(item.get("pollution_summary_json"), {})
item["success_analysis"] = loader(item.get("success_analysis_json"), {})
item["failure_analysis"] = loader(item.get("failure_analysis_json"), {})
item["candidate_rules"] = loader(item.get("candidate_rules_json"), [])
item["release_decision"] = (item.get("release_decision") or "").strip()
item["release_reason"] = (item.get("release_reason") or "").strip()
item["confidence_level"] = (item.get("confidence_level") or "").strip()
item["promotion_state"] = (item.get("promotion_state") or "research_only").strip()
item["strategy_version"] = (item.get("strategy_version") or "").strip()
item["version_change_summary"] = (item.get("version_change_summary") or "").strip()
result.append(item)
return result
def get_strategy_iteration_summary(days=30, conn_provider=None, json_loader=None):
conn_factory = conn_provider or get_conn
loader = json_loader or _loads_json_field
conn = conn_factory()
cutoff = (datetime.now() - timedelta(days=float(days or 30))).isoformat()
rows = conn.execute(
"""
SELECT * FROM strategy_iteration_log
WHERE created_at >= %s
ORDER BY created_at DESC, id DESC
""",
(cutoff,),
).fetchall()
rec_rows = conn.execute(
"""
SELECT strategy_version, status, pnl_pct, max_pnl_pct, max_drawdown_pct
FROM recommendation
WHERE strategy_version IS NOT NULL AND trim(strategy_version) != ''
"""
).fetchall()
conn.close()
def classify_recommendation_result(row):
status = row.get("status") or ""
pnl_pct = row.get("pnl_pct") or 0
max_pnl_pct = row.get("max_pnl_pct") or 0
max_drawdown_pct = row.get("max_drawdown_pct") or 0
if status in ("hit_tp1", "hit_tp2"):
return "success"
if status == "stopped_out":
return "failed"
if status == "expired":
if max_pnl_pct >= 5:
return "success"
if pnl_pct <= -3 or max_drawdown_pct <= -5:
return "failed"
return "pending"
if status == "active":
if max_pnl_pct >= 5:
return "success"
if pnl_pct <= -3 or max_drawdown_pct <= -5:
return "failed"
return "pending"
return "pending"
version_stats_map = {}
for row in rec_rows:
item = dict(row)
strategy_version = (item.get("strategy_version") or "").strip()
if not strategy_version:
continue
bucket = version_stats_map.setdefault(
strategy_version,
{
"strategy_version": strategy_version,
"recommendation_count": 0,
"success_count": 0,
"failed_count": 0,
"pending_count": 0,
"pnl_values": [],
},
)
bucket["recommendation_count"] += 1
bucket["pnl_values"].append(float(item.get("pnl_pct") or 0))
outcome = classify_recommendation_result(item)
if outcome == "success":
bucket["success_count"] += 1
elif outcome == "failed":
bucket["failed_count"] += 1
else:
bucket["pending_count"] += 1
logs = []
trigger_counts = {}
changed_rule_count = 0
unique_days = set()
titles = []
problem_keywords = {}
total_config_change_count = 0
hit_rates = []
avg_pnls = []
version_changelog = []
for row in rows:
item = dict(row)
item["findings"] = loader(item.get("findings_json"), [])
item["problems"] = loader(item.get("problems_json"), [])
item["actions"] = loader(item.get("actions_json"), [])
item["changed_rules"] = loader(item.get("changed_rules_json"), [])
item["metrics"] = loader(item.get("metrics_json"), {})
item["related_symbols"] = loader(item.get("related_symbols_json"), [])
item["config_diff"] = loader(item.get("config_diff_json"), {})
item["effect_summary"] = loader(item.get("effect_summary_json"), {})
item["pollution_summary"] = loader(item.get("pollution_summary_json"), {})
item["success_analysis"] = loader(item.get("success_analysis_json"), {})
item["failure_analysis"] = loader(item.get("failure_analysis_json"), {})
item["candidate_rules"] = loader(item.get("candidate_rules_json"), [])
item["release_decision"] = (item.get("release_decision") or "").strip()
item["release_reason"] = (item.get("release_reason") or "").strip()
item["confidence_level"] = (item.get("confidence_level") or "").strip()
item["promotion_state"] = (item.get("promotion_state") or "research_only").strip()
item["strategy_version"] = (item.get("strategy_version") or "").strip()
item["version_change_summary"] = (item.get("version_change_summary") or "").strip()
logs.append(item)
unique_days.add(item.get("run_date") or (item.get("created_at") or "")[:10])
trigger = item.get("trigger_source") or "unknown"
trigger_counts[trigger] = trigger_counts.get(trigger, 0) + 1
changed_rule_count += len(item.get("changed_rules") or [])
if item.get("title"):
titles.append(item["title"])
for problem in item.get("problems") or []:
key = str(problem).strip()
if key:
problem_keywords[key] = problem_keywords.get(key, 0) + 1
diff = item.get("config_diff") or {}
total_config_change_count += len(diff.get("changed") or []) + len(diff.get("added") or []) + len(diff.get("removed") or [])
effect = item.get("effect_summary") or {}
if isinstance(effect.get("hit_rate_pct"), (int, float)):
hit_rates.append(effect.get("hit_rate_pct"))
if isinstance(effect.get("avg_pnl"), (int, float)):
avg_pnls.append(effect.get("avg_pnl"))
if item.get("strategy_version"):
version_changelog.append({
"strategy_version": item.get("strategy_version"),
"created_at": item.get("created_at"),
"run_date": item.get("run_date"),
"title": item.get("title") or "",
"summary": item.get("summary") or "",
"version_change_summary": item.get("version_change_summary") or "",
"changed_rules_count": len(item.get("changed_rules") or []),
"config_change_count": len(diff.get("changed") or []) + len(diff.get("added") or []) + len(diff.get("removed") or []),
})
top_problems = sorted(problem_keywords.items(), key=lambda x: (-x[1], x[0]))[:5]
def _version_sort_key(version):
nums = [int(x) for x in re.findall(r"\d+", str(version or ""))]
return tuple(nums) if nums else (0,)
version_stats = []
for strategy_version, bucket in sorted(version_stats_map.items(), key=lambda kv: _version_sort_key(kv[0]), reverse=True):
resolved = bucket["success_count"] + bucket["failed_count"]
version_stats.append({
"strategy_version": strategy_version,
"recommendation_count": bucket["recommendation_count"],
"success_count": bucket["success_count"],
"failed_count": bucket["failed_count"],
"pending_count": bucket["pending_count"],
"success_rate_pct": round(bucket["success_count"] / resolved * 100, 1) if resolved else 0,
"avg_pnl_pct": round(sum(bucket["pnl_values"]) / len(bucket["pnl_values"]), 2) if bucket["pnl_values"] else 0,
})
return {
"days": days,
"total_logs": len(logs),
"unique_run_days": len(unique_days),
"trigger_counts": trigger_counts,
"change_rule_count": changed_rule_count,
"config_change_count": total_config_change_count,
"recent_titles": titles[:8],
"top_problems": [{"problem": k, "count": v} for k, v in top_problems],
"version_stats": version_stats,
"version_changelog": version_changelog[:12],
"effect_overview": {
"avg_hit_rate_pct": round(sum(hit_rates) / len(hit_rates), 1) if hit_rates else 0,
"avg_pnl": round(sum(avg_pnls) / len(avg_pnls), 2) if avg_pnls else 0,
"samples": len(logs),
},
}
__all__ = [
"backfill_strategy_failure_patterns",
"dry_run_strategy_candidate_performance",
"generate_candidates_from_review_history",
"get_strategy_failure_patterns",
"get_strategy_insights",
"get_strategy_iteration_dashboard",
"get_strategy_iteration_logs",
"get_strategy_iteration_summary",
"get_strategy_rule_candidates",
"log_strategy_iteration",
"refresh_strategy_candidate_performance",
]