Add key-level auto trade, AI analysis, and trading UX improvements.
Key monitors use 5m close triggers with WeChat alerts and box/convergence auto orders; add pending-order worker, structured WeChat notify, AI settings/messages, session clock, CTP margin sizing, and dual-layer position limits. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -296,6 +296,15 @@ def init_db():
|
||||
"ALTER TABLE order_plans ADD COLUMN decision_reason TEXT",
|
||||
"ALTER TABLE key_monitors ADD COLUMN status TEXT DEFAULT 'active'",
|
||||
"ALTER TABLE key_monitors ADD COLUMN archived_at TEXT",
|
||||
"ALTER TABLE key_monitors ADD COLUMN trade_mode TEXT DEFAULT '顺势'",
|
||||
"ALTER TABLE key_monitors ADD COLUMN risk_reward REAL DEFAULT 2",
|
||||
"ALTER TABLE key_monitors ADD COLUMN trailing_be INTEGER DEFAULT 0",
|
||||
"ALTER TABLE key_monitors ADD COLUMN last_trigger_bar TEXT",
|
||||
"ALTER TABLE key_monitors ADD COLUMN alert_push_count INTEGER DEFAULT 0",
|
||||
"ALTER TABLE key_monitors ADD COLUMN alert_last_push_at TEXT",
|
||||
"ALTER TABLE key_monitors ADD COLUMN alert_break_side TEXT",
|
||||
"ALTER TABLE key_monitors ADD COLUMN breakout_bar_time TEXT",
|
||||
"ALTER TABLE key_monitors ADD COLUMN alert_close_price REAL",
|
||||
"ALTER TABLE review_records ADD COLUMN direction TEXT",
|
||||
"ALTER TABLE review_records ADD COLUMN entry_price REAL",
|
||||
"ALTER TABLE review_records ADD COLUMN stop_loss REAL",
|
||||
@@ -411,10 +420,30 @@ def init_db():
|
||||
set_setting("risk_percent", "1")
|
||||
if not get_setting("max_margin_pct"):
|
||||
set_setting("max_margin_pct", "30")
|
||||
if not get_setting("roll_max_margin_pct"):
|
||||
set_setting("roll_max_margin_pct", "50")
|
||||
if not get_setting("trailing_be_tick_buffer"):
|
||||
set_setting("trailing_be_tick_buffer", "2")
|
||||
if not get_setting("pending_order_timeout_min"):
|
||||
set_setting("pending_order_timeout_min", "5")
|
||||
if not get_setting("ai_enabled"):
|
||||
set_setting("ai_enabled", "0")
|
||||
if not get_setting("ai_provider"):
|
||||
set_setting("ai_provider", "ollama")
|
||||
if not get_setting("ai_ollama_base_url"):
|
||||
set_setting("ai_ollama_base_url", "http://127.0.0.1:11434")
|
||||
if not get_setting("ai_ollama_model"):
|
||||
set_setting("ai_ollama_model", "qwen2.5:7b")
|
||||
if not get_setting("ai_openai_base_url"):
|
||||
set_setting("ai_openai_base_url", "https://api.openai.com/v1")
|
||||
if not get_setting("ai_openai_model"):
|
||||
set_setting("ai_openai_model", "gpt-4o-mini")
|
||||
if not get_setting("ai_daily_report_enabled"):
|
||||
set_setting("ai_daily_report_enabled", "1")
|
||||
if not get_setting("ai_daily_report_hour"):
|
||||
set_setting("ai_daily_report_hour", "15")
|
||||
if not get_setting("ai_daily_report_minute"):
|
||||
set_setting("ai_daily_report_minute", "5")
|
||||
if not get_setting("backup_auto_enabled"):
|
||||
set_setting("backup_auto_enabled", "1")
|
||||
if not get_setting("backup_auto_hour"):
|
||||
@@ -651,51 +680,23 @@ def check_order_plans():
|
||||
|
||||
|
||||
def check_key_monitors():
|
||||
from db_conn import DB_PATH
|
||||
from key_monitor_lib import run_key_monitor_check
|
||||
from trading_context import get_trading_mode
|
||||
|
||||
conn = get_db()
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM key_monitors WHERE status='active' OR status IS NULL"
|
||||
).fetchall()
|
||||
|
||||
for r in rows:
|
||||
sym = r["symbol"]
|
||||
typ = r["monitor_type"]
|
||||
up = r["upper"]
|
||||
low = r["lower"]
|
||||
up_trig = r["upper_triggered"]
|
||||
low_trig = r["lower_triggered"]
|
||||
name = r["symbol_name"] or sym
|
||||
pid = r["id"]
|
||||
sina = r["sina_code"] if "sina_code" in r.keys() else ""
|
||||
market = r["market_code"] if "market_code" in r.keys() else ""
|
||||
|
||||
p = fetch_price(sym, market, sina)
|
||||
if not p:
|
||||
continue
|
||||
|
||||
if typ in ("箱体突破", "收敛突破"):
|
||||
if p > up and not up_trig:
|
||||
send_wechat_msg(f"{name} 突破{typ}上沿 {up}\n当前价:{p}")
|
||||
conn.execute(
|
||||
"UPDATE key_monitors SET upper_triggered=1 WHERE id=?", (pid,)
|
||||
)
|
||||
if p < low and not low_trig:
|
||||
send_wechat_msg(f"{name} 跌破{typ}下沿 {low}\n当前价:{p}")
|
||||
conn.execute(
|
||||
"UPDATE key_monitors SET lower_triggered=1 WHERE id=?", (pid,)
|
||||
)
|
||||
elif typ == "关键阻力位" and p > up and not up_trig:
|
||||
send_wechat_msg(f"{name} 突破阻力位 {up}\n当前价:{p}")
|
||||
conn.execute(
|
||||
"UPDATE key_monitors SET upper_triggered=1 WHERE id=?", (pid,)
|
||||
)
|
||||
elif typ == "关键支撑位" and p < low and not low_trig:
|
||||
send_wechat_msg(f"{name} 跌破支撑位 {low}\n当前价:{p}")
|
||||
conn.execute(
|
||||
"UPDATE key_monitors SET lower_triggered=1 WHERE id=?", (pid,)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
try:
|
||||
execute_fn = getattr(app, "_execute_key_breakout", None)
|
||||
run_key_monitor_check(
|
||||
conn,
|
||||
db_path=DB_PATH,
|
||||
get_trading_mode_fn=lambda: get_trading_mode(get_setting),
|
||||
send_wechat=send_wechat_msg,
|
||||
execute_breakout_fn=execute_fn,
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def background_task():
|
||||
@@ -1039,6 +1040,20 @@ def del_plan(pid):
|
||||
return redirect(url_for("plans"))
|
||||
|
||||
|
||||
@app.route("/ai")
|
||||
@login_required
|
||||
@require_nav("ai")
|
||||
def ai_messages_page():
|
||||
from ai_messages import list_ai_messages
|
||||
|
||||
conn = get_db()
|
||||
try:
|
||||
messages = list_ai_messages(conn, limit=100)
|
||||
finally:
|
||||
conn.close()
|
||||
return render_template("ai_messages.html", messages=messages)
|
||||
|
||||
|
||||
@app.route("/keys")
|
||||
@login_required
|
||||
def keys():
|
||||
@@ -1058,23 +1073,52 @@ def keys():
|
||||
@login_required
|
||||
def add_key():
|
||||
d = request.form
|
||||
direction = d.get("direction")
|
||||
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()
|
||||
if not direction:
|
||||
flash("请选择多空方向")
|
||||
return redirect(url_for("keys"))
|
||||
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
|
||||
|
||||
direction = (d.get("direction") or "").strip().lower()
|
||||
if monitor_type in ("箱体突破", "收敛突破"):
|
||||
direction = direction or "long"
|
||||
else:
|
||||
direction = direction or "long"
|
||||
|
||||
conn = get_db()
|
||||
conn.execute(
|
||||
"""INSERT INTO key_monitors
|
||||
(symbol, symbol_name, market_code, sina_code, monitor_type, direction, upper, lower)
|
||||
VALUES (?,?,?,?,?,?,?,?)""",
|
||||
(symbol, symbol_name, market_code, sina_code, d["type"], direction, float(d["upper"]), float(d["lower"])),
|
||||
(symbol, symbol_name, market_code, sina_code, monitor_type, direction,
|
||||
upper, lower, trade_mode, risk_reward, trailing_be)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
symbol, symbol_name, market_code, sina_code, monitor_type, direction,
|
||||
upper, lower, trade_mode, risk_reward, trailing_be,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -1747,6 +1791,29 @@ def settings():
|
||||
webhook = request.form.get("wechat_webhook", "").strip()
|
||||
set_setting("wechat_webhook", webhook)
|
||||
flash("企业微信配置已保存")
|
||||
elif action == "ai":
|
||||
set_setting("ai_enabled", "1" if request.form.get("ai_enabled") else "0")
|
||||
provider = (request.form.get("ai_provider") or "ollama").strip().lower()
|
||||
if provider not in ("ollama", "openai"):
|
||||
provider = "ollama"
|
||||
set_setting("ai_provider", provider)
|
||||
set_setting("ai_ollama_base_url", (request.form.get("ai_ollama_base_url") or "").strip())
|
||||
set_setting("ai_ollama_model", (request.form.get("ai_ollama_model") or "").strip())
|
||||
set_setting("ai_openai_base_url", (request.form.get("ai_openai_base_url") or "").strip())
|
||||
key = (request.form.get("ai_openai_api_key") or "").strip()
|
||||
if key:
|
||||
set_setting("ai_openai_api_key", key)
|
||||
set_setting("ai_openai_model", (request.form.get("ai_openai_model") or "").strip())
|
||||
set_setting("ai_daily_report_enabled", "1" if request.form.get("ai_daily_report_enabled") else "0")
|
||||
try:
|
||||
set_setting("ai_daily_report_hour", str(max(0, min(23, int(request.form.get("ai_daily_report_hour", "15") or 15)))))
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
set_setting("ai_daily_report_minute", str(max(0, min(59, int(request.form.get("ai_daily_report_minute", "5") or 5)))))
|
||||
except ValueError:
|
||||
pass
|
||||
flash("AI 配置已保存")
|
||||
elif action == "trading":
|
||||
mode = request.form.get("trading_mode", "simulation").strip()
|
||||
if mode not in ("simulation", "live"):
|
||||
@@ -1781,6 +1848,12 @@ def settings():
|
||||
except ValueError:
|
||||
flash("保证金比例无效")
|
||||
return redirect(url_for("settings"))
|
||||
try:
|
||||
rmp = float(request.form.get("roll_max_margin_pct", "50") or 50)
|
||||
set_setting("roll_max_margin_pct", str(max(1.0, min(100.0, rmp))))
|
||||
except ValueError:
|
||||
flash("滚仓保证金比例无效")
|
||||
return redirect(url_for("settings"))
|
||||
try:
|
||||
tb = int(float(request.form.get("trailing_be_tick_buffer", "2") or 2))
|
||||
set_setting("trailing_be_tick_buffer", str(max(1, min(20, tb))))
|
||||
@@ -1893,6 +1966,7 @@ def settings():
|
||||
except Exception:
|
||||
pass
|
||||
from ctp_settings import get_ctp_settings_for_ui, is_ctp_auto_connect_enabled
|
||||
from product_recommend import small_account_margin_recommendations
|
||||
|
||||
return render_template(
|
||||
"settings.html",
|
||||
@@ -1908,6 +1982,8 @@ def settings():
|
||||
fixed_amount=get_setting("fixed_amount", "5000"),
|
||||
risk_percent=get_setting("risk_percent", "1"),
|
||||
max_margin_pct=get_setting("max_margin_pct", "30"),
|
||||
roll_max_margin_pct=get_setting("roll_max_margin_pct", "50"),
|
||||
small_account_margin_rec=small_account_margin_recommendations(),
|
||||
trailing_be_tick_buffer=get_setting("trailing_be_tick_buffer", "2"),
|
||||
pending_order_timeout_min=get_setting("pending_order_timeout_min", "5"),
|
||||
nav_items=get_nav_items(get_setting),
|
||||
@@ -1920,6 +1996,16 @@ def settings():
|
||||
backup_auto_hour=get_setting("backup_auto_hour", "3"),
|
||||
backup_keep_count=get_setting("backup_keep_count", "30"),
|
||||
backup_restore_dir=default_restore_dir(),
|
||||
ai_enabled=get_setting("ai_enabled", "0") == "1",
|
||||
ai_provider=get_setting("ai_provider", "ollama"),
|
||||
ai_ollama_base_url=get_setting("ai_ollama_base_url", "http://127.0.0.1:11434"),
|
||||
ai_ollama_model=get_setting("ai_ollama_model", "qwen2.5:7b"),
|
||||
ai_openai_base_url=get_setting("ai_openai_base_url", "https://api.openai.com/v1"),
|
||||
ai_openai_api_key=get_setting("ai_openai_api_key", ""),
|
||||
ai_openai_model=get_setting("ai_openai_model", "gpt-4o-mini"),
|
||||
ai_daily_report_enabled=get_setting("ai_daily_report_enabled", "1") == "1",
|
||||
ai_daily_report_hour=get_setting("ai_daily_report_hour", "15"),
|
||||
ai_daily_report_minute=get_setting("ai_daily_report_minute", "5"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user