alphax/app/db/altcoin_db.py
2026-05-27 07:02:37 +08:00

347 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
山寨币监控 — 数据库层
全量记录筛选结果 + 价格跟踪 + 盈亏验证
"""
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.get_conn = get_conn
_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 downgrade_active_entries_for_market_risk(*args, **kwargs):
_sync_command_compat_hooks()
return _recommendation_commands.downgrade_active_entries_for_market_risk(*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}")