本地监控止盈止损、盘前自动连CTP,并完善保证金与推荐手数。

- 止盈止损改为程序本地监控,触发后市价平仓(含跳空)
- 交易前30分钟后台自动连接 CTP
- 保证金占用上限默认30%,可在系统设置修改
- K线标准蜡烛图红跌绿涨,费率表全宽固定表头
- 品种推荐按保证金比例×总资金计算推荐手数

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-25 12:18:18 +08:00
parent fe1b651900
commit 9875ee6d44
15 changed files with 467 additions and 256 deletions
+45 -25
View File
@@ -16,20 +16,22 @@ from position_sizing import (
MODE_RISK,
DEFAULT_MAX_ORDER_LOTS,
calc_lots_by_risk,
calc_margin_usage_pct,
calc_order_tick_metrics,
normalize_sizing_mode,
)
from recommend_store import load_recommend_cache, recommend_payload, refresh_recommend_cache
from recommend_stream import recommend_hub, start_recommend_worker
from ctp_reconnect import start_ctp_reconnect_worker
from ctp_premarket_connect import start_ctp_premarket_connect_worker
from ctp_fee_worker import start_ctp_fee_worker
from sl_tp_guard import (
cancel_monitor_exit_orders,
ensure_monitor_order_columns,
monitor_order_status,
place_monitor_exit_orders,
reconcile_monitors_without_position,
start_sl_tp_guard_worker,
sync_all_sl_tp_orders,
)
from risk.account_risk_lib import (
assert_can_open,
@@ -50,6 +52,7 @@ from trading_context import (
TRADING_MODE_LIVE,
TRADING_MODE_SIM,
get_account_capital,
get_max_margin_pct,
get_risk_percent,
get_sizing_mode,
get_trading_mode,
@@ -79,6 +82,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
"trading_mode": get_trading_mode(get_setting),
"position_sizing_mode": get_sizing_mode(get_setting),
"risk_percent": str(get_risk_percent(get_setting)),
"max_margin_pct": str(get_max_margin_pct(get_setting)),
}
def _capital(conn) -> float:
@@ -194,14 +198,14 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
pending.append({
**base,
"order_kind": "stop_loss",
"label": "止损挂单",
"label": "止损监控",
"price": float(sl),
})
if tp is not None:
pending.append({
**base,
"order_kind": "take_profit",
"label": "止盈挂单",
"label": "止盈监控",
"price": float(tp),
})
ctp_st = ctp_status(mode)
@@ -311,16 +315,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
order_st = monitor_order_status(
mon or {}, mode=mode, ths_code=sym, direction=direction,
)
can_place = bool(
mon
and (mon.get("stop_loss") is not None or mon.get("take_profit") is not None)
and (order_st.get("needs_sl_order") or order_st.get("needs_tp_order"))
)
pending_for_row: list[dict] = []
if sl is not None:
pending_for_row.append({
"order_kind": "stop_loss",
"label": "止损挂单",
"label": "止损监控",
"price": sl,
"lots": lots,
"source": "monitor",
@@ -329,7 +328,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
if tp is not None:
pending_for_row.append({
"order_kind": "take_profit",
"label": "止盈挂单",
"label": "止盈监控",
"price": tp,
"lots": lots,
"source": "monitor",
@@ -360,9 +359,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
"est_fee_close": fee_info["close_fee"],
"est_fee_close_type": fee_info["close_type"],
"est_pnl_net": est_net,
"sl_order_active": order_st.get("sl_order_active"),
"tp_order_active": order_st.get("tp_order_active"),
"can_place_orders": can_place,
"sl_order_active": order_st.get("sl_monitoring"),
"tp_order_active": order_st.get("tp_monitoring"),
"sl_monitoring": order_st.get("sl_monitoring"),
"tp_monitoring": order_st.get("tp_monitoring"),
"can_place_orders": False,
"tick_value_total": tick.get("tick_value_total"),
"price_precision": tick.get("price_precision"),
"tick_size": tick.get("tick_size"),
@@ -414,6 +415,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
sizing_mode=sizing,
sizing_mode_label="以损定仓" if sizing == MODE_RISK else "固定张数",
risk_percent=get_risk_percent(get_setting),
max_margin_pct=get_max_margin_pct(get_setting),
recommend_rows=rec_cache.get("rows") or [],
recommend_updated_at=rec_cache.get("updated_at"),
)
@@ -531,20 +533,14 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
mon_row = conn.execute(
"SELECT * FROM trade_order_monitors WHERE id=?", (mid,),
).fetchone()
if mon_row and (sl is not None or tp is not None):
try:
ensure_monitor_order_columns(conn)
place_monitor_exit_orders(conn, dict(mon_row), mode=mode, force=False)
except Exception as exc:
logger.warning("补充止盈止损后自动委托失败: %s", exc)
return jsonify({"ok": True, "monitor_id": mid, "message": "止盈止损已保存"})
return jsonify({"ok": True, "monitor_id": mid, "message": "止盈止损已保存,程序本地监控"})
finally:
conn.close()
@app.route("/api/trading/monitor/place-orders", methods=["POST"])
@login_required
def api_trading_monitor_place_orders():
"""按开仓快照向 CTP 挂止盈止损平仓委托"""
"""本地监控模式:清理旧版柜台挂单,不再向交易所挂止盈止损。"""
d = request.get_json(silent=True) or {}
try:
monitor_id = int(d.get("monitor_id") or 0)
@@ -757,8 +753,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
capital = _capital(conn)
conn.close()
sizing = get_sizing_mode(get_setting)
margin_pct = get_max_margin_pct(get_setting)
if sizing == MODE_RISK:
lots, err = calc_lots_by_risk(entry, sl, direction, capital, get_risk_percent(get_setting), sym)
lots, err = calc_lots_by_risk(
entry, sl, direction, capital, get_risk_percent(get_setting), sym,
max_margin_pct=margin_pct,
)
if err:
return jsonify({"ok": False, "error": err}), 400
else:
@@ -815,11 +815,26 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
return jsonify({"ok": False, "error": "以损定仓模式须填写止损价"}), 400
lots_calc, err = calc_lots_by_risk(
price, sl, direction, _capital(conn), get_risk_percent(get_setting), sym,
max_margin_pct=get_max_margin_pct(get_setting),
)
if err:
conn.close()
return jsonify({"ok": False, "error": err}), 400
lots = lots_calc or lots
margin_pct = get_max_margin_pct(get_setting)
usage = calc_margin_usage_pct(
_ctp_positions(mode),
_capital(conn),
extra_symbol=sym if offset.startswith("open") else "",
extra_lots=lots if offset.startswith("open") else 0,
extra_price=price if offset.startswith("open") else 0,
)
if offset.startswith("open") and usage > margin_pct:
conn.close()
return jsonify({
"ok": False,
"error": f"保证金占用 {usage:.1f}% 超过上限 {margin_pct:g}%(可在系统设置修改)",
}), 403
if lots > DEFAULT_MAX_ORDER_LOTS:
conn.close()
return jsonify({
@@ -881,9 +896,9 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
if mon_row and (sl or tp):
try:
ensure_monitor_order_columns(conn)
place_monitor_exit_orders(conn, dict(mon_row), mode=mode, force=False)
cancel_monitor_exit_orders(conn, dict(mon_row), mode=mode)
except Exception as exc:
logger.warning("开仓后自动挂止盈止损失败: %s", exc)
logger.warning("清理旧版止盈止损挂单失败: %s", exc)
conn.commit()
send_wechat_msg(f"{trading_mode_label(get_setting)} {offset} {sym} {direction} {lots}手 @{price}")
conn.close()
@@ -1011,7 +1026,10 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
init_strategy_tables(conn)
capital = _capital(conn)
mode = get_trading_mode(get_setting)
rows = refresh_recommend_cache(conn, capital, _main_quote, trading_mode=mode)
rows = refresh_recommend_cache(
conn, capital, _main_quote, trading_mode=mode,
max_margin_pct=get_max_margin_pct(get_setting),
)
payload = recommend_payload(conn, live_capital=capital)
recommend_hub.broadcast("recommend", {"ok": True, **payload})
return jsonify({"ok": True, "count": len(rows), **payload})
@@ -1345,8 +1363,10 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
quote_fn=_main_quote,
init_tables_fn=_init_tables,
get_mode_fn=lambda: get_trading_mode(get_setting),
get_max_margin_pct_fn=lambda: get_max_margin_pct(get_setting),
)
start_ctp_reconnect_worker(get_mode_fn=lambda: get_trading_mode(get_setting))
start_ctp_premarket_connect_worker(get_mode_fn=lambda: get_trading_mode(get_setting))
start_sl_tp_guard_worker(
db_path=DB_PATH,
get_mode_fn=lambda: get_trading_mode(get_setting),