# Copyright (c) 2025-2026 马建军. All rights reserved. """HTTP routes for market 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.core.symbols import ( list_main_contracts_grouped, list_recommended_symbols_grouped, search_symbols, ) from modules.market.kline_chart import MARKET_PERIODS, fetch_market_klines from modules.market.kline_stream import kline_hub, sse_format from modules.market.market import get_quote_source_label from queue import Empty @app.route("/api/symbols/search") @login_required def api_symbol_search(): q = request.args.get("q", "") conn = get_db() try: from modules.core.trading_context import get_account_capital, is_ctp_connected capital = get_account_capital(conn, get_setting) ctp_connected = is_ctp_connected(get_setting) finally: conn.close() return jsonify(search_symbols(q, capital=capital, ctp_connected=ctp_connected)) @app.route("/api/symbols/mains") @login_required def api_symbols_mains(): return jsonify(list_main_contracts_grouped()) @app.route("/api/symbols/recommended") @login_required def api_symbols_recommended(): """品种下拉:仅展示当前资金下可开仓品种(与下方可开仓品种表一致)。""" from modules.trading.recommend_store import recommend_payload from modules.core.trading_context import ( get_fixed_lots, get_max_margin_pct, get_recommend_capital, get_sizing_mode, get_trading_mode, ) conn = get_db() try: capital = get_recommend_capital(conn, get_setting) payload = recommend_payload( conn, live_capital=capital, max_margin_pct=get_max_margin_pct(get_setting), trading_mode=get_trading_mode(get_setting), sizing_mode=get_sizing_mode(get_setting), fixed_lots=get_fixed_lots(get_setting), ) return jsonify(list_recommended_symbols_grouped(payload.get("rows") or [])) finally: conn.close() @app.route("/market") @login_required @require_nav("market") def market_page(): symbol = request.args.get("symbol", "").strip() period = request.args.get("period", "15m").strip() valid = {p["key"] for p in MARKET_PERIODS} if period not in valid: period = "15m" ctp_st = {} try: from modules.ctp.vnpy_bridge import ctp_status from modules.core.trading_context import get_trading_mode ctp_st = ctp_status(get_trading_mode(get_setting)) except Exception: pass return render_template( "market.html", symbol=symbol, period=period, market_periods=MARKET_PERIODS, quote_label=get_quote_source_label(ctp_connected=bool(ctp_st.get("connected"))), ctp_connected=bool(ctp_st.get("connected")), ) @app.route("/api/kline") @login_required def api_kline(): symbol = request.args.get("symbol", "").strip() period = request.args.get("period", "15m").strip() if not symbol: return jsonify({"error": "请提供合约代码"}), 400 try: from modules.core.trading_context import get_trading_mode data = fetch_market_klines( symbol, period, DB_PATH, prefer_ctp=False, ) except Exception as exc: app.logger.warning("kline api failed: %s", exc) return jsonify({"error": str(exc)}), 500 if not data.get("chart_symbol"): return jsonify({"error": "无法识别合约代码"}), 400 if not data.get("bars"): return jsonify({"error": "未获取到K线数据,请稍后重试或更换合约"}), 404 return jsonify(data) @app.route("/api/kline/stream") @login_required def api_kline_stream(): from queue import Empty symbol = request.args.get("symbol", "").strip() period = request.args.get("period", "15m").strip() market_code = request.args.get("market_code", "").strip() sina_code = request.args.get("sina_code", "").strip() if not symbol: return jsonify({"error": "请提供合约代码"}), 400 def generate(): sub = kline_hub.subscribe(symbol, period, market_code, sina_code) try: kline_data = fetch_market_klines( symbol, period, DB_PATH, prefer_ctp=False, ) if kline_data.get("bars"): yield sse_format("kline", kline_data) yield sse_format( "quote", build_market_quote_payload( symbol, market_code, sina_code, prefer_sina=True, ), ) while True: try: msg = sub.queue.get(timeout=20) yield sse_format(msg["event"], msg["data"]) except Empty: yield ": heartbeat\n\n" finally: kline_hub.unsubscribe(sub) return Response( stream_with_context(generate()), mimetype="text/event-stream", headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no", }, ) @app.route("/api/market_quote") @login_required def api_market_quote(): symbol = request.args.get("symbol", "").strip() market_code = request.args.get("market_code", "").strip() sina_code = request.args.get("sina_code", "").strip() if not symbol and not market_code: return jsonify({"error": "请提供合约"}), 400 return jsonify(build_market_quote_payload( symbol, market_code, sina_code, prefer_sina=True, )) @app.route("/contract") @login_required def contract_profile_page(): return redirect(url_for("positions")) @app.route("/api/contract_profile") @login_required def api_contract_profile(): return jsonify({"error": "品种简介功能已移除"}), 404