K线后台自动刷新并通过SSE推送到前端,移除轮询

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-15 17:33:31 +08:00
parent b804bd19a7
commit 65992eb35e
5 changed files with 327 additions and 134 deletions
+79 -17
View File
@@ -13,7 +13,7 @@ from werkzeug.utils import secure_filename
from dotenv import load_dotenv
from flask import (
Flask, render_template, request, redirect, url_for,
flash, session, jsonify,
flash, session, jsonify, Response, stream_with_context,
)
from werkzeug.security import check_password_hash, generate_password_hash
@@ -31,6 +31,7 @@ from fee_sync import sync_fees_from_akshare
from contract_profile import get_contract_profile
from stats_engine import STATS_VIEWS, load_stats_cache, refresh_stats_cache
from kline_store import ensure_kline_tables
from kline_stream import kline_hub, sse_format
from kline_chart import generate_review_kline_chart, fetch_market_klines, MARKET_PERIODS
from market import get_price as market_get_price, set_ths_refresh_token, get_quote_source_label
@@ -364,6 +365,29 @@ def sync_ths_token():
sync_ths_token()
def build_market_quote_payload(
symbol: str,
market_code: str = "",
sina_code: str = "",
) -> dict:
if not market_code or not sina_code:
codes = ths_to_codes(symbol)
if codes:
market_code = codes.get("market_code", "") or market_code
sina_code = codes.get("sina_code", "") or sina_code
price = market_get_price(market_code, sina_code)
name = symbol
codes = ths_to_codes(symbol)
if codes:
name = codes.get("name", symbol)
return {
"symbol": symbol,
"name": name,
"price": price,
}
# —————————————— 推送 ——————————————
def send_wechat_msg(content: str):
@@ -542,6 +566,17 @@ def background_task():
pass
time.sleep(3)
def start_background_threads():
threading.Thread(target=background_task, daemon=True).start()
threading.Thread(
target=lambda: kline_hub.worker_loop(DB_PATH, build_market_quote_payload),
daemon=True,
).start()
start_background_threads()
# —————————————— 登录 ——————————————
def login_required(f):
@@ -1334,6 +1369,48 @@ def api_kline():
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)
if kline_data.get("bars"):
yield sse_format("kline", kline_data)
yield sse_format(
"quote",
build_market_quote_payload(symbol, market_code, sina_code),
)
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():
@@ -1342,21 +1419,7 @@ def api_market_quote():
sina_code = request.args.get("sina_code", "").strip()
if not symbol and not market_code:
return jsonify({"error": "请提供合约"}), 400
if not market_code or not sina_code:
codes = ths_to_codes(symbol)
if codes:
market_code = codes.get("market_code", "") or market_code
sina_code = codes.get("sina_code", "") or sina_code
price = market_get_price(market_code, sina_code)
name = symbol
codes = ths_to_codes(symbol)
if codes:
name = codes.get("name", symbol)
return jsonify({
"symbol": symbol,
"name": name,
"price": price,
})
return jsonify(build_market_quote_payload(symbol, market_code, sina_code))
@app.route("/contract")
@@ -1491,5 +1554,4 @@ def settings():
# —————————————— 启动 ——————————————
if __name__ == "__main__":
threading.Thread(target=background_task, daemon=True).start()
app.run(host=HOST, port=PORT, debug=DEBUG)