diff --git a/lib/hub/hub_bridge_register.py b/lib/hub/hub_bridge_register.py index 4782b18..45cbe02 100644 --- a/lib/hub/hub_bridge_register.py +++ b/lib/hub/hub_bridge_register.py @@ -204,6 +204,59 @@ def register_hub_bridge_routes(app: Flask, app_module: Any) -> None: return jsonify({"ok": False, "msg": str(e), "version": 1, "enabled": True}), 500 return jsonify(_enrich_hub_protocol(snap, app_module=app_module)) + @app.route("/api/hub/options/stats") + def api_hub_options_stats(): + """中控汇总用:窗口化已实现盈亏统计(不含浮盈进净盈亏主指标以外的展示字段仍附带).""" + if not hub_bridge_token_ok(request.headers): + return jsonify({"ok": False, "msg": "未授权", "version": 1}), 401 + hub_ctx = app.config.get("HUB_CTX") or {} + cfg = hub_ctx.get("options_cfg") + if not isinstance(cfg, dict): + return jsonify({"ok": False, "msg": "期权未就绪", "version": 1}), 503 + from lib.options.options_history_lib import load_options_history + from lib.options.options_margin_mode_lib import is_coin_margin_mode + from lib.options.options_positions_lib import sum_options_net_pnl_usdc + from lib.options.options_stats_lib import ( + compute_options_stats_for_window, + history_pnl_to_usdt, + normalize_stats_window, + ) + + window = normalize_stats_window(request.args.get("window") or "7d") + ex = cfg.get("exchange_options") + if ex is None: + return jsonify({"ok": False, "msg": "交易所未就绪", "version": 1}), 503 + try: + raw_live = cfg["fetch_option_positions"](ex) + if raw_live is None: + return jsonify({"ok": False, "msg": "获取期权持仓失败", "version": 1}) + history = load_options_history(ex, cfg) + stats = compute_options_stats_for_window(history_pnl_to_usdt(history, ex), window) + open_float = sum_options_net_pnl_usdc(cfg, ex, raw_live) + net_realized = stats.get("net_realized_pnl") + try: + net_f = float(net_realized) if net_realized is not None else 0.0 + except (TypeError, ValueError): + net_f = 0.0 + total_pnl = None + if open_float is not None: + total_pnl = round(net_f + float(open_float), 4) + elif stats.get("total_closed"): + total_pnl = round(net_f, 4) + pnl_unit = "U" if is_coin_margin_mode() else "USDC" + return jsonify( + { + "ok": True, + "version": 1, + "pnl_unit": pnl_unit, + "open_float_pnl": open_float, + "total_pnl": total_pnl, + **stats, + } + ) + except Exception as e: + return jsonify({"ok": False, "msg": str(e) or "统计失败", "version": 1}), 500 + @app.route("/api/hub/options/close", methods=["POST"]) def api_hub_options_close(): """中控下发全平指令:客户端本地买一限价执行;无效买一拒绝.""" diff --git a/lib/options/options_stats_lib.py b/lib/options/options_stats_lib.py index 8936b0b..0cd0092 100644 --- a/lib/options/options_stats_lib.py +++ b/lib/options/options_stats_lib.py @@ -185,6 +185,81 @@ def compute_options_stats_from_history(history: list[dict[str, Any]]) -> dict[st } +_STATS_WINDOWS = ("today", "7d", "30d", "all") + + +def normalize_stats_window(raw: Any) -> str: + w = str(raw or "7d").strip().lower() + if w in ("1d", "day", "今日"): + return "today" + if w in ("7", "week"): + return "7d" + if w in ("30", "month"): + return "30d" + if w in ("0", "全部", "total"): + return "all" + return w if w in _STATS_WINDOWS else "7d" + + +def stats_window_bounds(window: str, *, now: datetime | None = None) -> tuple[datetime | None, datetime | None]: + """返回闭区间起止(本地/业务时区近似用本地 now).all → (None, None).""" + w = normalize_stats_window(window) + end = now or datetime.now() + if w == "all": + return None, None + if w == "today": + start = end.replace(hour=0, minute=0, second=0, microsecond=0) + return start, end + days = 7 if w == "7d" else 30 + from datetime import timedelta + + start = end - timedelta(days=days) + return start, end + + +def filter_history_for_stats_window( + history: list[dict[str, Any]], + window: str, + *, + now: datetime | None = None, +) -> list[dict[str, Any]]: + """窗口内已平仓 + 当前持仓(open).""" + start, end = stats_window_bounds(window, now=now) + out: list[dict[str, Any]] = [] + for row in history or []: + if row.get("status") == "open": + out.append(row) + continue + if start is None: + out.append(row) + continue + ts = _parse_ts(row.get("closed_at")) or _parse_ts(row.get("created_at")) + if ts is None: + continue + if ts < start: + continue + if end is not None and ts > end: + continue + out.append(row) + return out + + +def compute_options_stats_for_window( + history: list[dict[str, Any]], + window: str, + *, + now: datetime | None = None, +) -> dict[str, Any]: + w = normalize_stats_window(window) + filtered = filter_history_for_stats_window(history, w, now=now) + stats = compute_options_stats_from_history(filtered) + start, end = stats_window_bounds(w, now=now) + stats["window"] = w + stats["window_start"] = start.strftime("%Y-%m-%d %H:%M:%S") if start else None + stats["window_end"] = end.strftime("%Y-%m-%d %H:%M:%S") if end else None + return stats + + def compute_options_stats(get_db) -> dict[str, Any]: conn = get_db() try: