From 427f94e0e86e7debb354eba10d634dea93e0a78f Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 22 May 2026 11:29:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=89=8D=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto_monitor_gate/app.py | 220 +++++++++++++++++-------------- hub_bridge.py | 36 ++++- manual_trading_hub/hub.py | 79 +++++++++-- manual_trading_hub/static/app.js | 25 +++- 4 files changed, 246 insertions(+), 114 deletions(-) diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 04c5597..6ac0767 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -6343,112 +6343,138 @@ def api_key_kline(): @app.route("/add_key", methods=["POST"]) @login_required def add_key(): - d = request.form - symbol = normalize_symbol_input(d.get("symbol")) - if not symbol: - flash("symbol 不能为空") - return redirect("/key_monitor") - direction_sel = (d.get("direction") or "").strip().lower() - if direction_sel not in ("long", "short"): - flash("请选择做多或做空") - return redirect("/key_monitor") - mt = (d.get("type") or "").strip() - allowed_types = ( - tuple(KEY_MONITOR_AUTO_TYPES) - + tuple(KEY_MONITOR_ALERT_ONLY_TYPES) - + tuple(FIB_KEY_MONITOR_TYPES) - ) - if mt not in allowed_types: - flash("监控类型无效") - return redirect("/key_monitor") - rank, total = _daily_volume_rank(symbol) - if rank is None: - flash("日成交量排名读取失败,请稍后重试") - return redirect("/key_monitor") - if rank > KEY_DAILY_VOLUME_RANK_MAX: - flash(f"{symbol} 当前日成交量排名为 {rank}/{total},不在前{KEY_DAILY_VOLUME_RANK_MAX},已拒绝添加关键位") - return redirect("/key_monitor") - conn = get_db() - if mt in KEY_MONITOR_AUTO_TYPES: - occupied = get_active_position_count(conn) - if occupied >= MAX_ACTIVE_POSITIONS: - conn.close() + conn = None + try: + d = request.form + symbol = normalize_symbol_input(d.get("symbol")) + if not symbol: + flash("symbol 不能为空") + return redirect("/key_monitor") + direction_sel = (d.get("direction") or "").strip().lower() + if direction_sel not in ("long", "short"): + flash("请选择做多或做空") + return redirect("/key_monitor") + mt = (d.get("type") or "").strip() + allowed_types = ( + tuple(KEY_MONITOR_AUTO_TYPES) + + tuple(KEY_MONITOR_ALERT_ONLY_TYPES) + + tuple(FIB_KEY_MONITOR_TYPES) + ) + if mt not in allowed_types: + flash("监控类型无效") + return redirect("/key_monitor") + rank, total = _daily_volume_rank(symbol) + if rank is None: + flash("日成交量排名读取失败,请稍后重试") + return redirect("/key_monitor") + if rank > KEY_DAILY_VOLUME_RANK_MAX: flash( - f"当前持仓已达上限({occupied}/{MAX_ACTIVE_POSITIONS}):无法添加「箱体突破 / 收敛突破」。" - "请平仓后再试,或使用「关键阻力位/关键支撑位」(仅单次提醒)。" + f"{symbol} 当前日成交量排名为 {rank}/{total},不在前{KEY_DAILY_VOLUME_RANK_MAX},已拒绝添加关键位" ) return redirect("/key_monitor") - ex_sym_key = normalize_exchange_symbol(symbol) - try: - ensure_markets_loaded() - except Exception: - pass - upper_px = round_price_to_exchange(ex_sym_key, float(d["upper"])) - lower_px = round_price_to_exchange(ex_sym_key, float(d["lower"])) - be_flag = parse_breakeven_enabled_form(d.get("breakeven_enabled")) - if is_fib_key_monitor_type(mt): - ok_fib, err_fib = _add_fib_key_monitor( - conn, symbol, direction_sel, mt, upper_px, lower_px, breakeven_enabled=be_flag, + conn = get_db() + if mt in KEY_MONITOR_AUTO_TYPES: + occupied = get_active_position_count(conn) + if occupied >= MAX_ACTIVE_POSITIONS: + conn.close() + conn = None + flash( + f"当前持仓已达上限({occupied}/{MAX_ACTIVE_POSITIONS}):无法添加「箱体突破 / 收敛突破」。" + "请平仓后再试,或使用「关键阻力位/关键支撑位」(仅单次提醒)。" + ) + return redirect("/key_monitor") + ex_sym_key = normalize_exchange_symbol(symbol) + try: + ensure_markets_loaded() + except Exception: + pass + try: + upper_raw = float(d.get("upper") or 0) + lower_raw = float(d.get("lower") or 0) + except (TypeError, ValueError): + conn.close() + conn = None + flash("上下沿须为有效数字") + return redirect("/key_monitor") + upper_px = round_price_to_exchange(ex_sym_key, upper_raw) + lower_px = round_price_to_exchange(ex_sym_key, lower_raw) + be_flag = parse_breakeven_enabled_form(d.get("breakeven_enabled")) + if is_fib_key_monitor_type(mt): + ok_fib, err_fib = _add_fib_key_monitor( + conn, symbol, direction_sel, mt, upper_px, lower_px, breakeven_enabled=be_flag, + ) + conn.commit() + conn.close() + conn = None + if not ok_fib: + flash(err_fib or "斐波监控添加失败") + return redirect("/key_monitor") + flash( + f"斐波监控已添加,限价单已挂出({symbol} 日成交量排名 {rank}/{total})" + f"|移动保本:{'开' if be_flag else '关'}" + ) + return redirect("/key_monitor") + sl_tp_mode = "standard" + manual_tp = None + if mt in KEY_MONITOR_AUTO_TYPES: + sl_tp_mode = normalize_sl_tp_mode(d.get("sl_tp_mode")) + if sl_tp_mode == "trend_manual": + try: + manual_tp = float(d.get("manual_take_profit") or 0) + except (TypeError, ValueError): + manual_tp = 0 + if manual_tp <= 0: + conn.close() + conn = None + flash("趋势单方案须填写有效止盈价") + return redirect("/key_monitor") + if direction_sel == "long" and manual_tp <= upper_px: + conn.close() + conn = None + flash("做多趋势单:止盈价应高于上沿(阻力)") + return redirect("/key_monitor") + if direction_sel == "short" and manual_tp >= lower_px: + conn.close() + conn = None + flash("做空趋势单:止盈价应低于下沿(支撑)") + return redirect("/key_monitor") + mtpx = round_price_to_exchange(ex_sym_key, manual_tp) + if mtpx is not None: + manual_tp = float(mtpx) + conn.execute( + "INSERT INTO key_monitors " + "(symbol,monitor_type,direction,upper,lower,sl_tp_mode,manual_take_profit,breakeven_enabled) " + "VALUES (?,?,?,?,?,?,?,?)", + (symbol, mt, direction_sel, upper_px, lower_px, sl_tp_mode, manual_tp, be_flag), ) conn.commit() conn.close() - if not ok_fib: - flash(err_fib or "斐波监控添加失败") - return redirect("/key_monitor") - flash( - f"斐波监控已添加,限价单已挂出({symbol} 日成交量排名 {rank}/{total})" - f"|移动保本:{'开' if be_flag else '关'}" - ) + conn = None + ctr = False + try: + coin4h_status, _, _ = _status_by_ema55(symbol, "4h") + ctr = (direction_sel == "long" and coin4h_status == "空头") or ( + direction_sel == "short" and coin4h_status == "多头" + ) + except Exception: + pass + extra = "" + if mt in KEY_MONITOR_AUTO_TYPES: + extra = f"|方案:{sl_tp_mode_label(sl_tp_mode)}|移动保本:{'开' if be_flag else '关'}" + flash(f"添加成功({symbol} 日成交量排名 {rank}/{total}){extra}") + if ctr: + flash( + "⚠️ 4h EMA55 提示:当前与所选方向逆势;「箱体突破/收敛突破」在条件满足时仍会按计划自动市价开仓,请注意仓位。" + ) return redirect("/key_monitor") - sl_tp_mode = "standard" - manual_tp = None - if mt in KEY_MONITOR_AUTO_TYPES: - sl_tp_mode = normalize_sl_tp_mode(d.get("sl_tp_mode")) - if sl_tp_mode == "trend_manual": + except Exception as e: + if conn is not None: try: - manual_tp = float(d.get("manual_take_profit") or 0) - except (TypeError, ValueError): - manual_tp = 0 - if manual_tp <= 0: conn.close() - flash("趋势单方案须填写有效止盈价") - return redirect("/key_monitor") - if direction_sel == "long" and manual_tp <= upper_px: - conn.close() - flash("做多趋势单:止盈价应高于上沿(阻力)") - return redirect("/key_monitor") - if direction_sel == "short" and manual_tp >= lower_px: - conn.close() - flash("做空趋势单:止盈价应低于下沿(支撑)") - return redirect("/key_monitor") - mtpx = round_price_to_exchange(ex_sym_key, manual_tp) - if mtpx is not None: - manual_tp = float(mtpx) - conn.execute( - "INSERT INTO key_monitors " - "(symbol,monitor_type,direction,upper,lower,sl_tp_mode,manual_take_profit,breakeven_enabled) " - "VALUES (?,?,?,?,?,?,?,?)", - (symbol, mt, direction_sel, upper_px, lower_px, sl_tp_mode, manual_tp, be_flag), - ) - conn.commit() - conn.close() - ctr = False - try: - coin4h_status, _, _ = _status_by_ema55(symbol, "4h") - ctr = (direction_sel == "long" and coin4h_status == "空头") or ( - direction_sel == "short" and coin4h_status == "多头" - ) - except Exception: - pass - extra = "" - if mt in KEY_MONITOR_AUTO_TYPES: - extra = f"|方案:{sl_tp_mode_label(sl_tp_mode)}|移动保本:{'开' if be_flag else '关'}" - flash(f"添加成功({symbol} 日成交量排名 {rank}/{total}){extra}") - if ctr: - flash( - "⚠️ 4h EMA55 提示:当前与所选方向逆势;「箱体突破/收敛突破」在条件满足时仍会按计划自动市价开仓,请注意仓位。" - ) - return redirect("/key_monitor") + except Exception: + pass + flash(f"添加关键位失败:{e}") + return redirect("/key_monitor") @app.route("/add_order", methods=["POST"]) @login_required diff --git a/hub_bridge.py b/hub_bridge.py index 72a1ea0..8ca0476 100644 --- a/hub_bridge.py +++ b/hub_bridge.py @@ -38,27 +38,55 @@ def _row_to_dict(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)]} - msgs = [str(x) for x in get_flashed_messages()] + 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 ("失败", "错误", "拒绝", "无效", "缺少", "无法", "过期")): + 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, *, @@ -155,12 +183,12 @@ def register_hub_routes(app): @app.route("/api/hub/add_order", methods=["POST"]) @_hub_auth_required def api_hub_add_order(): - return jsonify(_invoke_view("add_order", "/trade")) + return _hub_json("add_order", "/add_order") @app.route("/api/hub/add_key", methods=["POST"]) @_hub_auth_required def api_hub_add_key(): - return jsonify(_invoke_view("add_key", "/key_monitor")) + return _hub_json("add_key", "/add_key") @app.route("/api/hub/trend/preview", methods=["POST"]) @_hub_auth_required diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index aab196b..8d1fa45 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -182,6 +182,46 @@ async def _fetch_agent_status(client: httpx.AsyncClient, ex: dict) -> dict: } +def _exchange_brief(ex: dict | None) -> dict | None: + if not ex: + return None + return { + "id": ex.get("id"), + "key": ex.get("key"), + "name": ex.get("name"), + } + + +def _form_plain_dict(form) -> dict[str, str]: + out: dict[str, str] = {} + for k, v in form.multi_items() if hasattr(form, "multi_items") else form.items(): + if hasattr(v, "read"): + continue + out[str(k)] = "" if v is None else str(v) + return out + + +def _parse_http_json_body(r: httpx.Response) -> dict: + text = (r.text or "").strip() + if not text: + return {"ok": False, "status": r.status_code, "text": "(empty body)"} + try: + data = r.json() + if isinstance(data, dict): + return data + return {"ok": False, "status": r.status_code, "text": text[:500]} + except Exception: + snippet = text[:500] + if snippet.lstrip().lower().startswith(" dict | None: @@ -194,8 +234,11 @@ async def _fetch_flask_json( else: r = await client.post(f"{base}{path}", headers=_hub_headers(), data=data, timeout=120.0) if r.status_code >= 400: - return {"ok": False, "status": r.status_code, "text": (r.text or "")[:500]} - return r.json() + parsed = _parse_http_json_body(r) + parsed.setdefault("ok", False) + parsed.setdefault("status", r.status_code) + return parsed + return _parse_http_json_body(r) except Exception as e: return {"ok": False, "error": str(e)} @@ -308,10 +351,18 @@ async def api_trade_order(exchange_id: str, request: Request): ex = _find_exchange(exchange_id) if not ex or not ex.get("enabled"): raise HTTPException(status_code=404, detail="账户未启用") - form = await request.form() - async with httpx.AsyncClient() as client: - result = await _fetch_flask_json(client, ex, "/api/hub/add_order", "POST", dict(form)) - return {"exchange": ex, "result": result} + try: + form = _form_plain_dict(await request.form()) + async with httpx.AsyncClient() as client: + result = await _fetch_flask_json(client, ex, "/api/hub/add_order", "POST", form) + return {"exchange": _exchange_brief(ex), "result": result} + except HTTPException: + raise + except Exception as e: + return JSONResponse( + {"exchange": _exchange_brief(ex), "result": {"ok": False, "messages": [str(e)]}}, + status_code=200, + ) @app.post("/api/trade/key/{exchange_id}") @@ -321,10 +372,18 @@ async def api_trade_key(exchange_id: str, request: Request): raise HTTPException(status_code=404, detail="账户未启用") if "key" not in (ex.get("capabilities") or []): raise HTTPException(status_code=400, detail="该账户不支持关键位") - form = await request.form() - async with httpx.AsyncClient() as client: - result = await _fetch_flask_json(client, ex, "/api/hub/add_key", "POST", dict(form)) - return {"exchange": ex, "result": result} + try: + form = _form_plain_dict(await request.form()) + async with httpx.AsyncClient() as client: + result = await _fetch_flask_json(client, ex, "/api/hub/add_key", "POST", form) + return {"exchange": _exchange_brief(ex), "result": result} + except HTTPException: + raise + except Exception as e: + return JSONResponse( + {"exchange": _exchange_brief(ex), "result": {"ok": False, "messages": [str(e)]}}, + status_code=200, + ) @app.post("/api/trade/trend/preview/{exchange_id}") diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 3c94f60..ea90543 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -294,15 +294,34 @@ } } + async function parseJsonResponse(r) { + const text = await r.text(); + if (!text) return {}; + try { + return JSON.parse(text); + } catch (e) { + const snippet = text.slice(0, 200); + throw new Error( + `HTTP ${r.status} 响应不是 JSON:${snippet}${text.length > 200 ? "…" : ""}` + ); + } + } + async function submitForm(path, formEl) { const id = document.getElementById("trade-account").value; const fd = new FormData(formEl); try { const r = await fetch(path + encodeURIComponent(id), { method: "POST", body: fd }); - const j = await r.json(); + const j = await parseJsonResponse(r); const res = j.result || {}; - const msgs = (res.messages || []).join("\n") || JSON.stringify(res, null, 2); - showToast(msgs, !res.ok); + const msgs = + (res.messages || []).join("\n") || + res.error || + res.msg || + res.text || + JSON.stringify(res, null, 2); + const failed = res.ok === false || r.status >= 400 || !!res.error || !!res.text; + showToast(msgs || (failed ? "操作失败" : "已提交"), failed); if (res.ok && res.preview) { showTrendPreview(res); }