# Copyright (c) 2025-2026 马建军. All rights reserved. """HTTP routes for stats module.""" from __future__ import annotations from datetime import date, datetime from flask import ( Response, flash, jsonify, redirect, render_template, request, send_file, session, stream_with_context, url_for, ) def register(deps) -> None: app = deps.app login_required = deps.login_required require_nav = deps.require_nav get_db = deps.get_db get_setting = deps.get_setting set_setting = deps.set_setting fetch_price = deps.fetch_price send_wechat_msg = deps.send_wechat_msg touch_stats_cache = deps.touch_stats_cache get_stats_data = deps.get_stats_data build_market_quote_payload = deps.build_market_quote_payload today_str = deps.today_str expire_old_plans = deps.expire_old_plans TZ = deps.tz DB_PATH = deps.db_path UPLOAD_DIR = deps.upload_dir OPEN_TYPES = deps.open_types EXIT_TRIGGERS = deps.exit_triggers BEHAVIOR_TAGS = deps.behavior_tags KLINE_PERIODS = deps.kline_periods KLINE_CUTOFFS = deps.kline_cutoffs calc_holding_duration = deps.calc_holding_duration holding_to_minutes = deps.holding_to_minutes classify_close_result = deps.classify_close_result calc_rr_ratio = deps.calc_rr_ratio calc_theoretical_pnl = deps.calc_theoretical_pnl parse_review_date_filter = deps.parse_review_date_filter _trading_mode = deps.trading_mode _ua_is_phone = deps.ua_is_phone _static_asset_v = deps.static_asset_v from modules.stats.stats_engine import ( STATS_VIEWS, get_calendar_day, get_calendar_month, refresh_stats_cache, ) from modules.settings.nav_settings import nav_enabled from modules.stats.dashboard_lib import build_dashboard_payload from modules.core.doc_render import read_doc, render_markdown _dashboard_sync_tick = {"n": 0} @app.route("/stats") @login_required def stats(): return render_template("stats.html") @app.route("/calendar") @login_required def trade_calendar(): return render_template("calendar.html") @app.route("/api/stats") @login_required def api_stats(): return jsonify(get_stats_data()) @app.route("/api/stats/views") @login_required def api_stats_views(): return jsonify({"views": STATS_VIEWS}) @app.route("/api/stats/refresh", methods=["POST"]) @login_required def api_stats_refresh(): conn = get_db() capital = float(get_setting("live_capital", "0") or 0) data = refresh_stats_cache(conn, capital) conn.close() return jsonify(data) @app.route("/api/stats/calendar") @login_required def api_stats_calendar(): now = datetime.now(TZ) year = request.args.get("year", type=int) or now.year month = request.args.get("month", type=int) or now.month if month < 1 or month > 12: return jsonify({"error": "invalid month"}), 400 conn = get_db() try: data = get_calendar_month(conn, year, month) finally: conn.close() return jsonify(data) @app.route("/api/stats/calendar/day") @login_required def api_stats_calendar_day(): day = (request.args.get("date") or "").strip() if not day: return jsonify({"error": "date required"}), 400 try: date.fromisoformat(day) except ValueError: return jsonify({"error": "invalid date"}), 400 conn = get_db() try: data = get_calendar_day(conn, day) finally: conn.close() return jsonify(data) @app.route("/dashboard") @login_required @require_nav("dashboard") def dashboard(): return render_template("dashboard.html") @app.route("/risk-guide") @login_required @require_nav("risk_guide") def risk_guide(): from modules.core.doc_render import read_doc, render_markdown try: _title, raw = read_doc("risk-guide") except FileNotFoundError: flash("文档不存在") return redirect(url_for("positions")) return render_template("risk_guide.html", doc_html=render_markdown(raw)) @app.route("/api/dashboard/live") @login_required def api_dashboard_live(): if not nav_enabled(get_setting, "dashboard"): return jsonify({"ok": False, "error": "数据看板已在系统设置中关闭"}), 403 from modules.stats.dashboard_lib import build_dashboard_payload _dashboard_sync_tick["n"] += 1 sync_trades = _dashboard_sync_tick["n"] % 15 == 0 try: payload = build_dashboard_payload( get_db=get_db, get_setting=get_setting, fetch_price=fetch_price, sync_ctp_trades=sync_trades, ) return jsonify(payload) except Exception as exc: app.logger.exception("dashboard live: %s", exc) return jsonify({"ok": False, "error": "看板数据暂时不可用"}), 503