341 lines
12 KiB
Python
341 lines
12 KiB
Python
"""
|
||
山寨币监控 — 数据库层
|
||
全量记录筛选结果 + 价格跟踪 + 盈亏验证
|
||
"""
|
||
|
||
import json
|
||
from datetime import datetime, timedelta
|
||
|
||
from app.config.config_loader import get_meta
|
||
from app.db import recommendation_commands as _recommendation_commands
|
||
from app.db.coin_state_queries import expire_old_states, get_all_active, update_state
|
||
from app.db.cron_queries import get_cron_run_logs, get_cron_run_summary, log_cron_run
|
||
from app.db.postgres_connection import apply_migrations, connect as pg_connect
|
||
from app.db.recommendation_state import (
|
||
classify_recommendation_result as _classify_recommendation_result,
|
||
derive_execution_fields as _derive_execution_fields,
|
||
is_actionable_execution_status as _is_actionable_execution_status,
|
||
is_executed_trade as _is_executed_trade,
|
||
)
|
||
from app.db.push_queries import PUSH_COOLDOWN_HOURS, get_recommendation_for_push, log_push, should_push
|
||
from app.db.review_basic_queries import (
|
||
get_signal_weights,
|
||
record_missed_explosion,
|
||
record_review,
|
||
update_signal_performance,
|
||
)
|
||
from app.db.screening_queries import get_candidates_for_confirm, get_screening_history, log_screening
|
||
from app.db.strategy_rule_queries import (
|
||
backfill_strategy_failure_patterns,
|
||
dry_run_strategy_candidate_performance,
|
||
generate_candidates_from_review_history,
|
||
get_strategy_failure_patterns,
|
||
get_strategy_iteration_dashboard,
|
||
get_strategy_rule_candidates,
|
||
record_strategy_failure_pattern,
|
||
refresh_strategy_candidate_performance,
|
||
update_strategy_rule_candidate_status,
|
||
upsert_strategy_rule_candidate,
|
||
)
|
||
from app.db.strategy_insights import get_strategy_insights
|
||
from app.db.tracking_queries import (
|
||
get_latest_price_cache,
|
||
update_entry_timing,
|
||
update_latest_price_cache,
|
||
update_recommendation_tracking,
|
||
)
|
||
|
||
def get_conn():
|
||
return pg_connect()
|
||
|
||
|
||
def init_db():
|
||
apply_migrations()
|
||
print("PostgreSQL schema migrations checked")
|
||
|
||
|
||
def _sync_command_compat_hooks():
|
||
"""Keep legacy altcoin_db monkeypatch hooks effective after command extraction."""
|
||
_recommendation_commands.get_meta = get_meta
|
||
_recommendation_commands.datetime = datetime
|
||
|
||
|
||
def create_recommendation(*args, **kwargs):
|
||
_sync_command_compat_hooks()
|
||
return _recommendation_commands.create_recommendation(*args, **kwargs)
|
||
|
||
|
||
def expire_old_recommendations(*args, **kwargs):
|
||
_sync_command_compat_hooks()
|
||
return _recommendation_commands.expire_old_recommendations(*args, **kwargs)
|
||
|
||
|
||
def apply_recommendation_state_transition(*args, **kwargs):
|
||
_sync_command_compat_hooks()
|
||
return _recommendation_commands.apply_recommendation_state_transition(*args, **kwargs)
|
||
|
||
|
||
def recompute_all_recommendation_state_fields(*args, **kwargs):
|
||
_sync_command_compat_hooks()
|
||
return _recommendation_commands.recompute_all_recommendation_state_fields(*args, **kwargs)
|
||
|
||
|
||
def update_recommendation_action_status(*args, **kwargs):
|
||
_sync_command_compat_hooks()
|
||
return _recommendation_commands.update_recommendation_action_status(*args, **kwargs)
|
||
|
||
|
||
# ==================== 查询API ====================
|
||
|
||
def get_active_recommendations(actionable_only=False):
|
||
"""获取所有active推荐。默认保留全量,实时页请使用去重视图的可执行口径。"""
|
||
conn = get_conn()
|
||
rows = conn.execute("""
|
||
SELECT * FROM recommendation
|
||
WHERE status='active' AND COALESCE(display_bucket,'watch_pool') != 'history'
|
||
ORDER BY rec_time DESC
|
||
""").fetchall()
|
||
conn.close()
|
||
result = []
|
||
for row in rows:
|
||
item = _derive_execution_fields(dict(row))
|
||
if actionable_only and not _is_actionable_execution_status(item.get("execution_status")):
|
||
continue
|
||
result.append(item)
|
||
return result
|
||
|
||
|
||
def get_active_recommendations_deduped(actionable_only=True, version="", hours=0, watch_symbols=None, limit=0, offset=0, with_meta=False):
|
||
"""获取去重后的active推荐(同symbol只保留最新一条),并附带推荐结果判定。
|
||
version 为空时不按版本过滤;hours>0 时只取最近 N 小时信号。
|
||
with_meta=True 时返回分页对象,兼容实时看板首屏分页加载。"""
|
||
conn = get_conn()
|
||
where = "status='active' AND COALESCE(display_bucket,'watch_pool') != 'history'"
|
||
params = []
|
||
version = str(version or "").strip()
|
||
if version:
|
||
where += " AND strategy_version=%s"
|
||
params.append(version)
|
||
if watch_symbols:
|
||
symbols = [str(s).strip().upper() for s in watch_symbols if str(s).strip()]
|
||
if symbols:
|
||
where += " AND symbol IN (" + ",".join(["%s"] * len(symbols)) + ")"
|
||
params.extend(symbols)
|
||
try:
|
||
hours = float(hours or 0)
|
||
except Exception:
|
||
hours = 0
|
||
if hours > 0:
|
||
cutoff = (datetime.now() - timedelta(hours=hours)).isoformat()
|
||
where += " AND rec_time >= %s"
|
||
params.append(cutoff)
|
||
|
||
try:
|
||
limit = max(0, int(limit or 0))
|
||
except Exception:
|
||
limit = 0
|
||
try:
|
||
offset = max(0, int(offset or 0))
|
||
except Exception:
|
||
offset = 0
|
||
|
||
rows = conn.execute(f"""
|
||
SELECT r.*,
|
||
lpc.price AS latest_cache_price,
|
||
lpc.updated_at AS latest_cache_updated_at
|
||
FROM recommendation r
|
||
LEFT JOIN latest_price_cache lpc ON lpc.symbol = r.symbol
|
||
JOIN (
|
||
SELECT symbol, MAX(id) AS max_id
|
||
FROM recommendation
|
||
WHERE {where}
|
||
GROUP BY symbol
|
||
) latest ON latest.max_id = r.id
|
||
ORDER BY r.rec_time DESC
|
||
""", tuple(params)).fetchall()
|
||
conn.close()
|
||
|
||
all_items = []
|
||
# 实时看板只输出当前有效机会;过期/失效样本属于历史/复盘,不再进入实时列表或 summary。
|
||
summary = {
|
||
"buy_now": 0,
|
||
"wait_pullback": 0,
|
||
"observe": 0,
|
||
"observe_strong": 0,
|
||
"observe_weak": 0,
|
||
"expired": 0,
|
||
"total": 0,
|
||
"discovery_burst": 0,
|
||
"executable_now": 0,
|
||
"planned_entry": 0,
|
||
"watch_pool": 0,
|
||
}
|
||
now = datetime.now()
|
||
for row in rows:
|
||
item = dict(row)
|
||
rec_result, rec_result_label = _classify_recommendation_result(item)
|
||
item["recommendation_result"] = rec_result
|
||
item["recommendation_result_label"] = rec_result_label
|
||
_derive_execution_fields(item)
|
||
|
||
is_expired = False
|
||
if hours > 0:
|
||
try:
|
||
rec_time = item.get("rec_time")
|
||
if rec_time:
|
||
is_expired = (now - datetime.fromisoformat(str(rec_time))).total_seconds() > hours * 3600
|
||
except Exception:
|
||
is_expired = False
|
||
if item.get("execution_status") == "invalid" or item.get("status") in ("invalid", "expired", "archived") or item.get("display_bucket") == "history":
|
||
is_expired = True
|
||
|
||
# 带 hours 的实时看板请求必须过滤旧/脏/过期;不带 hours 的内部/测试查询保留全量派生结果。
|
||
if is_expired:
|
||
summary["expired"] += 1
|
||
continue
|
||
|
||
if actionable_only and not _is_actionable_execution_status(item.get("execution_status")):
|
||
continue
|
||
all_items.append(item)
|
||
if item.get("is_discovery_burst"):
|
||
summary["discovery_burst"] += 1
|
||
if item.get("is_executable_now"):
|
||
summary["executable_now"] += 1
|
||
if item.get("execution_status") == "wait_pullback":
|
||
summary["planned_entry"] += 1
|
||
if item.get("is_watch_pool"):
|
||
summary["watch_pool"] += 1
|
||
|
||
if item.get("execution_status") == "buy_now":
|
||
summary["buy_now"] += 1
|
||
elif item.get("execution_status") == "wait_pullback":
|
||
summary["wait_pullback"] += 1
|
||
else:
|
||
summary["observe"] += 1
|
||
if item.get("observe_tier") == "weak":
|
||
summary["observe_weak"] += 1
|
||
else:
|
||
summary["observe_strong"] += 1
|
||
summary["total"] = len(all_items)
|
||
# expired 仅作内部审计计数,不属于实时机会流;API 对外不暴露,避免前端/用户继续看到过期入口。
|
||
summary["expired_filtered"] = summary.pop("expired", 0)
|
||
|
||
if not with_meta:
|
||
try:
|
||
from app.services.llm_insights import attach_recommendation_insights
|
||
return attach_recommendation_insights(all_items)
|
||
except Exception:
|
||
return all_items
|
||
page_items = all_items[offset: offset + limit] if limit else all_items[offset:]
|
||
try:
|
||
from app.services.llm_insights import attach_recommendation_insights
|
||
attach_recommendation_insights(page_items)
|
||
except Exception:
|
||
pass
|
||
return {
|
||
"items": page_items,
|
||
"total": len(all_items),
|
||
"limit": limit,
|
||
"offset": offset,
|
||
"has_more": bool(limit and offset + len(page_items) < len(all_items)),
|
||
"summary": summary,
|
||
}
|
||
|
||
|
||
def get_all_recommendations(limit=50, decision_only=False, version="", offset=0, with_meta=False):
|
||
"""兼容导出:推荐列表查询已迁移到 analytics 模块。"""
|
||
from app.db.analytics import get_all_recommendations as _get_all_recommendations
|
||
|
||
return _get_all_recommendations(
|
||
limit=limit,
|
||
decision_only=decision_only,
|
||
version=version,
|
||
offset=offset,
|
||
with_meta=with_meta,
|
||
)
|
||
|
||
|
||
def get_stats():
|
||
"""兼容导出:统计聚合已迁移到 analytics 模块。"""
|
||
from app.db.analytics import get_stats as _get_stats
|
||
|
||
return _get_stats()
|
||
|
||
|
||
def get_review_stats():
|
||
"""兼容导出:复盘统计已迁移到 analytics 模块。"""
|
||
from app.db.analytics import get_review_stats as _get_review_stats
|
||
|
||
return _get_review_stats(
|
||
conn_provider=get_conn,
|
||
iteration_logs_getter=get_strategy_iteration_logs,
|
||
iteration_summary_getter=get_strategy_iteration_summary,
|
||
)
|
||
|
||
|
||
def _loads_json_field(value, fallback):
|
||
try:
|
||
return json.loads(value) if isinstance(value, str) else (value if value is not None else fallback)
|
||
except Exception:
|
||
return fallback
|
||
|
||
|
||
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"):
|
||
"""兼容导出:策略迭代写入已迁移到 review_queries 模块。"""
|
||
from app.db.review_queries import log_strategy_iteration as _log_strategy_iteration
|
||
|
||
return _log_strategy_iteration(
|
||
run_date=run_date,
|
||
trigger_source=trigger_source,
|
||
title=title,
|
||
summary=summary,
|
||
findings=findings,
|
||
problems=problems,
|
||
actions=actions,
|
||
changed_rules=changed_rules,
|
||
metrics=metrics,
|
||
related_symbols=related_symbols,
|
||
config_diff=config_diff,
|
||
effect_summary=effect_summary,
|
||
pollution_summary=pollution_summary,
|
||
strategy_version=strategy_version,
|
||
version_change_summary=version_change_summary,
|
||
success_analysis=success_analysis,
|
||
failure_analysis=failure_analysis,
|
||
candidate_rules=candidate_rules,
|
||
release_decision=release_decision,
|
||
release_reason=release_reason,
|
||
confidence_level=confidence_level,
|
||
promotion_state=promotion_state,
|
||
conn_provider=get_conn,
|
||
)
|
||
|
||
|
||
def get_strategy_iteration_logs(limit=30):
|
||
"""兼容导出:策略迭代日志查询已迁移到 review_queries 模块。"""
|
||
from app.db.review_queries import get_strategy_iteration_logs as _get_strategy_iteration_logs
|
||
|
||
return _get_strategy_iteration_logs(limit=limit, conn_provider=get_conn, json_loader=_loads_json_field)
|
||
|
||
|
||
|
||
|
||
def get_strategy_iteration_summary(days=30):
|
||
"""兼容导出:策略迭代汇总已迁移到 review_queries 模块。"""
|
||
from app.db.review_queries import get_strategy_iteration_summary as _get_strategy_iteration_summary
|
||
|
||
return _get_strategy_iteration_summary(days=days, conn_provider=get_conn, json_loader=_loads_json_field)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
init_db()
|
||
stats = get_stats()
|
||
print(f"DB初始化完成: {stats}")
|