e5a586f903
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>
231 lines
8.0 KiB
Python
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
|