合并期货下单与持仓监控为统一界面,移除手工录入。
策略与 CTP 自动同步持仓,新增 /api/trading/live 聚合展示与平仓接口。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+238
-5
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user