""" 各 crypto_monitor_* 注册 /api/hub/* JSON 接口,供 manual_trading_hub 调用。 实例末尾:app.config["HUB_CTX"] = {...}; register_hub_routes(app) """ from __future__ import annotations import json import time from functools import wraps from flask import current_app, get_flashed_messages, jsonify, request, session from hub_auth import request_allowed def _hub_auth_required(f): @wraps(f) def wrapped(*args, **kwargs): from flask import current_app as cap auth_disabled = bool(cap.config.get("HUB_AUTH_DISABLED")) if not request_allowed(bool(session.get("logged_in")), auth_disabled): return jsonify({"ok": False, "msg": "未授权(登录或 HUB_BRIDGE_TOKEN)"}), 401 return f(*args, **kwargs) return wrapped def _ctx(): return current_app.config.get("HUB_CTX") or {} def _row_to_dict(row): fn = _ctx().get("row_to_dict") if fn and row is not None: return fn(row) return dict(row) if row is not None else {} _FAIL_HINTS = ( "失败", "错误", "拒绝", "无效", "缺少", "无法", "过期", "未达", "不能为空", "已有", "不允许", "异常", ) def _invoke_view(view_name: str, path: str, form=None) -> dict: views = _ctx().get("views") or {} view = views.get(view_name) if not view: return {"ok": False, "messages": [f"未配置视图 {view_name}"]} data = form if form is not None else request.form if hasattr(data, "items") and not isinstance(data, dict): data = {k: v for k, v in data.items()} with current_app.test_request_context(path, method="POST", data=data): session["logged_in"] = True try: view() except Exception as e: return {"ok": False, "messages": [str(e)]} try: msgs = [str(x) for x in get_flashed_messages()] except Exception as e: return {"ok": False, "messages": [f"读取提示信息失败: {e}"]} ok = True for m in msgs: if any(k in m for k in _FAIL_HINTS): ok = False break return {"ok": ok, "messages": msgs} def _hub_json(view_name: str, path: str, form=None): try: return jsonify(_invoke_view(view_name, path, form=form)) except Exception as e: return jsonify({"ok": False, "messages": [str(e)]}) def install_on_app( app, *, exchange: str, capabilities: list, has_trend: bool, get_db, row_to_dict, meta_fn, views: dict, ): app.config["HUB_CTX"] = { "exchange": exchange, "capabilities": list(capabilities), "has_trend": bool(has_trend), "get_db": get_db, "row_to_dict": row_to_dict, "meta_fn": meta_fn, "views": views, } register_hub_routes(app) def register_hub_routes(app): auth_disabled = False try: import os auth_disabled = os.getenv("APP_AUTH_DISABLED", "false").lower() in ( "1", "true", "yes", "on", ) except Exception: pass app.config.setdefault("HUB_AUTH_DISABLED", auth_disabled) @app.route("/api/hub/ping") @_hub_auth_required def api_hub_ping(): c = _ctx() return jsonify( { "ok": True, "exchange": c.get("exchange"), "capabilities": c.get("capabilities") or [], } ) @app.route("/api/hub/meta") @_hub_auth_required def api_hub_meta(): c = _ctx() meta_fn = c.get("meta_fn") meta = meta_fn() if callable(meta_fn) else {} return jsonify({"ok": True, "meta": meta}) @app.route("/api/hub/monitor") @_hub_auth_required def api_hub_monitor(): c = _ctx() get_db = c.get("get_db") if not get_db: return jsonify({"ok": False, "msg": "HUB_CTX 缺少 get_db"}), 500 conn = get_db() keys = [] for row in conn.execute("SELECT * FROM key_monitors ORDER BY id DESC").fetchall(): keys.append(_row_to_dict(row)) orders = [] for row in conn.execute( "SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC" ).fetchall(): orders.append(_row_to_dict(row)) trends = [] if c.get("has_trend"): for row in conn.execute( "SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC" ).fetchall(): trends.append(_row_to_dict(row)) conn.close() enrich = c.get("enrich_monitor") if callable(enrich): try: payload = enrich(keys=keys, orders=orders, trends=trends) if isinstance(payload, dict): return jsonify({"ok": True, **payload}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 return jsonify( {"ok": True, "keys": keys, "orders": orders, "trends": trends, "key_prices": []} ) @app.route("/api/hub/add_order", methods=["POST"]) @_hub_auth_required def api_hub_add_order(): return _hub_json("add_order", "/add_order") @app.route("/api/hub/add_key", methods=["POST"]) @_hub_auth_required def api_hub_add_key(): return _hub_json("add_key", "/add_key") @app.route("/api/hub/trend/preview", methods=["POST"]) @_hub_auth_required def api_hub_trend_preview(): if not _ctx().get("has_trend"): return jsonify({"ok": False, "msg": "该实例无趋势回调"}), 400 data = _invoke_view("preview_trend_pullback", "/trade") pid = _latest_preview_id() preview = _fetch_preview(pid) if pid else None return jsonify( { "ok": bool(data.get("ok")), "messages": data.get("messages") or [], "preview_id": pid, "preview": preview, } ) @app.route("/api/hub/trend/execute", methods=["POST"]) @_hub_auth_required def api_hub_trend_execute(): if not _ctx().get("has_trend"): return jsonify({"ok": False, "msg": "该实例无趋势回调"}), 400 pid = (request.form.get("preview_id") or "").strip() if not pid: body = request.get_json(silent=True) or {} pid = str(body.get("preview_id") or "").strip() form = {"preview_id": pid} if pid else {} return jsonify(_invoke_view("execute_trend_pullback", "/trade", form=form)) @app.route("/api/hub/trend/preview/") @_hub_auth_required def api_hub_trend_preview_get(pid): if not _ctx().get("has_trend"): return jsonify({"ok": False, "msg": "该实例无趋势回调"}), 400 preview = _fetch_preview(pid) if not preview: return jsonify({"ok": False, "msg": "预览不存在或已过期"}), 404 return jsonify({"ok": True, "preview": preview}) def _latest_preview_id(): get_db = _ctx().get("get_db") if not get_db: return None conn = get_db() row = conn.execute( "SELECT id FROM trend_pullback_previews ORDER BY created_at DESC LIMIT 1" ).fetchone() conn.close() return row["id"] if row else None def _fetch_preview(pid): get_db = _ctx().get("get_db") if not get_db or not pid: return None conn = get_db() row = conn.execute( "SELECT * FROM trend_pullback_previews WHERE id=?", (pid,) ).fetchone() conn.close() if not row: return None d = _row_to_dict(row) now_ms = int(time.time() * 1000) d["expires_in_sec"] = max(0, int((int(d.get("expires_at_ms") or 0) - now_ms) / 1000)) try: grid = json.loads(d.get("grid_prices_json") or "[]") legs = json.loads(d.get("leg_amounts_json") or "[]") d["grid_levels"] = [ {"i": i + 1, "price": grid[i], "contracts": legs[i] if i < len(legs) else None} for i in range(len(grid)) ] except Exception: d["grid_levels"] = [] return d