Files
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

186 lines
6.4 KiB
Python

# Copyright (c) 2025-2026 马建军. All rights reserved.
"""HTTP routes for keys 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
@app.route("/api/key_prices")
@login_required
def api_key_prices():
"""关键位监控列表:批量现价与距上/下沿距离。"""
conn = get_db()
rows = conn.execute(
"SELECT id, symbol, market_code, sina_code, upper, lower "
"FROM key_monitors WHERE status='active' OR status IS NULL"
).fetchall()
conn.close()
out = []
for r in rows:
sym = r["symbol"]
market = r["market_code"] or ""
sina = r["sina_code"] or ""
upper = float(r["upper"])
lower = float(r["lower"])
price = fetch_price(sym, market, sina)
dist_upper = None
dist_lower = None
if price is not None:
dist_upper = round(upper - price, 2)
dist_lower = round(price - lower, 2)
out.append({
"id": r["id"],
"price": price,
"dist_upper": dist_upper,
"dist_lower": dist_lower,
})
return jsonify(out)
@app.route("/keys")
@login_required
def keys():
from modules.keys.key_monitor_lib import key_monitor_periods
conn = get_db()
key_list = conn.execute(
"SELECT * FROM key_monitors WHERE status='active' OR status IS NULL ORDER BY id DESC"
).fetchall()
history = conn.execute(
"SELECT * FROM key_monitors WHERE status='archived' ORDER BY archived_at DESC LIMIT 100"
).fetchall()
conn.close()
return render_template(
"keys.html",
keys=key_list,
history=history,
key_periods=key_monitor_periods(),
)
@app.route("/add_key", methods=["POST"])
@login_required
def add_key():
d = request.form
symbol = d.get("symbol", "").strip()
symbol_name = d.get("symbol_name", "").strip()
market_code = d.get("market_code", "").strip()
sina_code = d.get("sina_code", "").strip()
monitor_type = (d.get("type") or "").strip()
if not symbol or not market_code:
flash("请从下拉列表选择品种(同花顺合约代码)")
return redirect(url_for("keys"))
try:
upper = float(d.get("upper") or 0)
lower = float(d.get("lower") or 0)
except (TypeError, ValueError):
flash("上沿/下沿价格无效")
return redirect(url_for("keys"))
if upper <= lower:
flash("上沿必须大于下沿")
return redirect(url_for("keys"))
trade_mode = (d.get("trade_mode") or "顺势").strip()
if trade_mode not in ("顺势", "反转"):
trade_mode = "顺势"
try:
risk_reward = float(d.get("risk_reward") or 2)
except (TypeError, ValueError):
risk_reward = 2.0
risk_reward = max(0.5, min(10.0, risk_reward))
trailing_be = 1 if d.get("trailing_be") else 0
if trailing_be and risk_reward < 3:
risk_reward = 3.0
from modules.keys.key_monitor_lib import normalize_bar_period
bar_period = normalize_bar_period(d.get("bar_period") or "5m")
direction = (d.get("direction") or "").strip().lower()
if monitor_type == "箱体突破":
if direction not in ("long", "short"):
flash("箱体突破须选择上方向(做多/做空)")
return redirect(url_for("keys"))
else:
direction = ""
conn = get_db()
conn.execute(
"""INSERT INTO key_monitors
(symbol, symbol_name, market_code, sina_code, monitor_type, direction,
upper, lower, trade_mode, risk_reward, trailing_be, bar_period)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
(
symbol, symbol_name, market_code, sina_code, monitor_type, direction,
upper, lower, trade_mode, risk_reward, trailing_be, bar_period,
),
)
conn.commit()
conn.close()
flash("关键位监控已添加")
return redirect(url_for("keys"))
@app.route("/add_position", methods=["POST"])
@login_required
def add_position():
flash("持仓由策略交易或 CTP 自动同步,无需手工录入")
return redirect(url_for("positions"))
@app.route("/del_key/<int:pid>")
@login_required
def del_key(pid):
conn = get_db()
conn.execute(
"UPDATE key_monitors SET status='archived', archived_at=? WHERE id=?",
(datetime.now(TZ).isoformat(), pid),
)
conn.commit()
conn.close()
flash("已移入监控历史")
return redirect(url_for("keys"))