合并期货下单与持仓监控为统一界面,移除手工录入。

策略与 CTP 自动同步持仓,新增 /api/trading/live 聚合展示与平仓接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-24 10:18:00 +08:00
parent 6e423eebfb
commit 7b8a660309
8 changed files with 396 additions and 233 deletions
+238 -5
View File
@@ -8,6 +8,7 @@ from typing import Any, Callable
from flask import flash, jsonify, redirect, render_template, request, url_for
from contract_specs import calc_position_metrics, get_contract_spec
from fee_specs import calc_fee_breakdown
from position_sizing import (
MODE_FIXED,
MODE_RISK,
@@ -98,32 +99,264 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
except Exception:
return False
def _holding_duration(open_time: str, now_iso: str) -> str:
try:
from app import calc_holding_duration
return calc_holding_duration(open_time, now_iso)
except Exception:
return ""
def _build_trading_live_rows(conn) -> list[dict]:
from zoneinfo import ZoneInfo
tz = ZoneInfo("Asia/Shanghai")
now_iso = datetime.now(tz).strftime("%Y-%m-%dT%H:%M")
capital = _capital(conn)
mode = get_trading_mode(get_setting)
ctp_st = ctp_status(mode)
rows: list[dict] = []
seen: set[str] = set()
ctp_pairs: list[tuple[str, str]] = []
if ctp_st.get("connected"):
for p in _ctp_positions(mode):
sym = (p.get("symbol") or "").strip()
direction = p.get("direction") or "long"
lots = int(p.get("lots") or 0)
if lots <= 0:
continue
ctp_pairs.append((sym, direction))
key = f"ctp:{sym.lower()}:{direction}"
seen.add(key)
entry = float(p.get("avg_price") or 0)
codes = ths_to_codes(sym)
mark = fetch_price(
sym,
codes.get("market_code", "") if codes else "",
codes.get("sina_code", "") if codes else "",
)
spec = get_contract_spec(sym)
mult = spec["mult"]
float_pnl = p.get("pnl")
if mark is not None and entry > 0:
if direction == "long":
float_pnl = round((mark - entry) * mult * lots, 2)
else:
float_pnl = round((entry - mark) * mult * lots, 2)
tick = calc_order_tick_metrics(sym, lots, mark or entry)
rows.append({
"key": key,
"source": "ctp",
"source_label": "CTP 柜台",
"symbol": codes.get("name", sym) if codes else sym,
"symbol_code": sym,
"direction": direction,
"direction_label": "做多" if direction == "long" else "做空",
"lots": lots,
"entry_price": entry,
"stop_loss": None,
"take_profit": None,
"mark_price": mark,
"float_pnl": float_pnl,
"tick_value_total": tick.get("tick_value_total"),
"price_precision": tick.get("price_precision"),
"tick_size": tick.get("tick_size"),
"can_close": True,
})
def _dup_ctp(ths_sym: str, direction: str) -> bool:
for cs, d in ctp_pairs:
if d != direction:
continue
if cs.lower() == ths_sym.lower() or _match_ctp_symbol(cs, ths_sym):
return True
return False
monitors = conn.execute(
"SELECT * FROM trade_order_monitors WHERE status='active' ORDER BY id DESC"
).fetchall()
for r in monitors:
sym = r["symbol"]
direction = r["direction"]
if _dup_ctp(sym, direction):
continue
key = f"mon:{sym.lower()}:{direction}"
entry = float(r["entry_price"] or 0)
sl = float(r["stop_loss"]) if r["stop_loss"] is not None else None
tp = float(r["take_profit"]) if r["take_profit"] is not None else None
lots = float(r["lots"] or 1)
codes = ths_to_codes(sym)
market = r["market_code"] or (codes.get("market_code", "") if codes else "") or ""
sina = codes.get("sina_code", "") if codes else ""
mark = fetch_price(sym, market, sina)
metrics = calc_position_metrics(direction, entry, sl or entry, tp or entry, lots, mark, capital, sym)
fee_info = calc_fee_breakdown(sym, entry, mark or entry, lots, r["open_time"] or "", now_iso)
est_net = None
if metrics.get("float_pnl") is not None:
est_net = round(metrics["float_pnl"] - fee_info["total_fee"], 2)
rows.append({
"key": key,
"source": "program",
"source_label": r["monitor_type"] or "程序监控",
"monitor_id": r["id"],
"symbol": r["symbol_name"] or sym,
"symbol_code": sym,
"direction": direction,
"direction_label": "做多" if direction == "long" else "做空",
"lots": lots,
"entry_price": entry,
"stop_loss": sl,
"take_profit": tp,
"mark_price": mark,
"open_time": r["open_time"],
"holding_duration": _holding_duration(r["open_time"] or "", now_iso),
"float_pnl": metrics.get("float_pnl"),
"float_pct": metrics.get("float_pct"),
"risk_pct": metrics.get("risk_pct"),
"risk_amount": metrics.get("risk_amount"),
"rr_ratio": metrics.get("rr_ratio"),
"margin": metrics.get("margin"),
"position_pct": metrics.get("position_pct"),
"est_fee": fee_info["total_fee"],
"est_pnl_net": est_net,
"can_close": ctp_st.get("connected"),
})
legacy = conn.execute(
"SELECT * FROM position_monitors WHERE status='active' ORDER BY id DESC"
).fetchall()
for r in legacy:
sym = r["symbol"]
direction = r["direction"]
key = f"leg:{sym.lower()}:{direction}"
if any(x.get("symbol_code", "").lower() == sym.lower() and x.get("direction") == direction for x in rows):
continue
entry = float(r["entry_price"])
sl = float(r["stop_loss"])
tp = float(r["take_profit"])
lots = float(r["lots"] or 1)
market = r["market_code"] or ""
sina = r["sina_code"] or ""
mark = fetch_price(sym, market, sina)
metrics = calc_position_metrics(direction, entry, sl, tp, lots, mark, capital, sym)
fee_info = calc_fee_breakdown(sym, entry, mark or entry, lots, r["open_time"] or "", now_iso)
est_net = None
if metrics.get("float_pnl") is not None:
est_net = round(metrics["float_pnl"] - fee_info["total_fee"], 2)
rows.append({
"key": key,
"source": "legacy",
"source_label": "历史录入",
"legacy_id": r["id"],
"symbol": r["symbol_name"] or sym,
"symbol_code": sym,
"direction": direction,
"direction_label": "做多" if direction == "long" else "做空",
"lots": lots,
"entry_price": entry,
"stop_loss": sl,
"take_profit": tp,
"mark_price": mark,
"open_time": r["open_time"],
"holding_duration": _holding_duration(r["open_time"] or "", now_iso),
"float_pnl": metrics.get("float_pnl"),
"float_pct": metrics.get("float_pct"),
"risk_pct": metrics.get("risk_pct"),
"risk_amount": metrics.get("risk_amount"),
"rr_ratio": metrics.get("rr_ratio"),
"margin": metrics.get("margin"),
"position_pct": metrics.get("position_pct"),
"est_fee": fee_info["total_fee"],
"est_pnl_net": est_net,
"can_close": True,
"close_url": f"/close_position/{r['id']}",
})
return rows
@app.route("/trade")
@login_required
def trade_page():
return redirect(url_for("positions"))
@app.route("/positions")
@login_required
def positions():
conn = get_db()
init_strategy_tables(conn)
mode = get_trading_mode(get_setting)
ctp_st = ctp_status(mode)
capital = _capital(conn)
sizing = get_sizing_mode(get_setting)
risk = get_risk_status(conn)
ctp_acc = _ctp_account(mode) if ctp_st.get("connected") else {}
positions = _ctp_positions(mode) if ctp_st.get("connected") else []
conn.close()
return render_template(
"trade.html",
trading_mode=mode,
trading_mode_label=trading_mode_label(get_setting),
sizing_mode=sizing,
risk_percent=get_risk_percent(get_setting),
capital=capital,
risk_status=risk,
ctp_status=ctp_st,
ctp_account=ctp_acc,
ctp_positions=positions,
)
@app.route("/api/trading/live")
@login_required
def api_trading_live():
conn = get_db()
init_strategy_tables(conn)
mode = get_trading_mode(get_setting)
ctp_st = ctp_status(mode)
rows = _build_trading_live_rows(conn)
conn.close()
return jsonify({
"rows": rows,
"capital": _capital(get_db()),
"ctp_status": ctp_st,
"trading_mode_label": trading_mode_label(get_setting),
})
@app.route("/api/trading/close", methods=["POST"])
@login_required
def api_trading_close():
d = request.get_json(silent=True) or {}
source = (d.get("source") or "").strip()
conn = get_db()
init_strategy_tables(conn)
mode = get_trading_mode(get_setting)
if not ctp_status(mode).get("connected") and source in ("ctp", "program"):
conn.close()
return jsonify({"ok": False, "error": "请先连接 CTP"}), 400
sym = (d.get("symbol_code") or d.get("symbol") or "").strip()
direction = (d.get("direction") or "long").strip().lower()
try:
lots = max(1, int(d.get("lots") or 1))
price = float(d.get("price") or 0)
except (TypeError, ValueError):
conn.close()
return jsonify({"ok": False, "error": "参数无效"}), 400
if not sym or price <= 0:
conn.close()
return jsonify({"ok": False, "error": "品种或价格无效"}), 400
offset = "close_long" if direction == "long" else "close_short"
try:
execute_order(
conn, mode=mode, offset=offset, symbol=sym, direction=direction,
lots=lots, price=price, settings=_settings_dict(),
)
if source == "program":
mid = int(d.get("monitor_id") or 0)
if mid:
conn.execute(
"UPDATE trade_order_monitors SET status='closed' WHERE id=?",
(mid,),
)
conn.commit()
conn.close()
return jsonify({"ok": True})
except ValueError as exc:
conn.close()
return jsonify({"ok": False, "error": str(exc)}), 400
@app.route("/recommend")
@login_required
def recommend_page():