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>
186 lines
6.4 KiB
Python
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"))
|
|
|