Files
qihuo/modules/market/routes.py
T
dekun e5a586f903 Restructure into modules/ with single-process CTP and config/ layout.
Move business code under modules/, env template to config/, PM2 single qihuo process, and _legacy shims for old imports.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 14:42:16 +08:00

231 lines
8.0 KiB
Python

# 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