完善下单表单与 CTP 持仓,requirements 加入 vnpy 并更新部署文档
以损定仓/固定张数分栏下单、限价市价、持仓仅读柜台;DEPLOY 补充 SimNow 与 vnpy 安装说明。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+47
-143
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user