""" 山寨币监控 — 数据库层 全量记录筛选结果 + 价格跟踪 + 盈亏验证 """ 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}")