完善下单表单与 CTP 持仓,requirements 加入 vnpy 并更新部署文档

以损定仓/固定张数分栏下单、限价市价、持仓仅读柜台;DEPLOY 补充 SimNow 与 vnpy 安装说明。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-24 10:52:25 +08:00
parent 709801305f
commit 62cd868f79
9 changed files with 394 additions and 308 deletions
+47 -143
View File
@@ -112,96 +112,48 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
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 not ctp_st.get("connected"):
return rows
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,
})
# 程序监控仅用于补充止损/止盈,持仓以 CTP 柜台为准
monitor_map: dict[tuple[str, str], dict] = {}
for r in conn.execute(
"SELECT * FROM trade_order_monitors WHERE status='active'"
).fetchall():
key = (r["symbol"].lower(), r["direction"])
monitor_map[key] = dict(r)
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):
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
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)
entry = float(p.get("avg_price") or 0)
float_pnl = p.get("pnl")
if float_pnl is not None:
float_pnl = round(float(float_pnl), 2)
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)
tick = calc_order_tick_metrics(sym, lots, entry)
mon = None
for (ms, md), mv in monitor_map.items():
if md != direction:
continue
if ms == sym.lower() or _match_ctp_symbol(sym, ms):
mon = mv
break
sl = float(mon["stop_loss"]) if mon and mon.get("stop_loss") is not None else None
tp = float(mon["take_profit"]) if mon and mon.get("take_profit") is not None else None
rows.append({
"key": key,
"source": "program",
"source_label": r["monitor_type"] or "程序监控",
"monitor_id": r["id"],
"symbol": r["symbol_name"] or sym,
"key": f"ctp:{sym.lower()}:{direction}",
"source": "ctp",
"source_label": "CTP 柜台",
"monitor_id": mon["id"] if mon else None,
"symbol": codes.get("name", sym) if codes else sym,
"symbol_code": sym,
"direction": direction,
"direction_label": "做多" if direction == "long" else "做空",
@@ -209,69 +161,12 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
"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,
"mark_price": None,
"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,
"close_url": f"/close_position/{r['id']}",
})
return rows
@@ -516,6 +411,14 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
price = float(d.get("price") or 0)
except (TypeError, ValueError):
return jsonify({"ok": False, "error": "手数或价格无效"}), 400
order_type = (d.get("order_type") or d.get("price_type") or "limit").strip().lower()
if order_type == "market" and price <= 0:
codes = ths_to_codes(sym)
price = fetch_price(
sym,
codes.get("market_code", "") if codes else "",
codes.get("sina_code", "") if codes else "",
) or 0
if not sym or price <= 0:
return jsonify({"ok": False, "error": "品种或价格无效"}), 400
conn = get_db()
@@ -547,6 +450,7 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
lots=lots,
price=price,
settings=_settings_dict(),
order_type=order_type,
)
if offset.startswith("open"):
sl = d.get("stop_loss")