合并期货下单与持仓监控为统一界面,移除手工录入。
策略与 CTP 自动同步持仓,新增 /api/trading/live 聚合展示与平仓接口。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -886,16 +886,6 @@ def keys():
|
||||
return render_template("keys.html", keys=key_list, history=history)
|
||||
|
||||
|
||||
@app.route("/positions")
|
||||
@login_required
|
||||
def positions():
|
||||
conn = get_db()
|
||||
pos_list = conn.execute(
|
||||
"SELECT * FROM position_monitors WHERE status='active' ORDER BY id DESC"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return render_template("positions.html", positions=pos_list)
|
||||
|
||||
|
||||
@app.route("/add_key", methods=["POST"])
|
||||
@login_required
|
||||
@@ -928,36 +918,7 @@ def add_key():
|
||||
@app.route("/add_position", methods=["POST"])
|
||||
@login_required
|
||||
def add_position():
|
||||
d = request.form
|
||||
symbol = d.get("symbol", "").strip()
|
||||
symbol_name = d.get("symbol_name", "").strip()
|
||||
market_code = d.get("market_code", "").strip()
|
||||
sina_code = d.get("sina_code", "").strip()
|
||||
if not symbol or not market_code:
|
||||
flash("请从下拉列表选择品种")
|
||||
return redirect(url_for("positions"))
|
||||
entry = float(d["entry_price"])
|
||||
sl = float(d["stop_loss"])
|
||||
tp = float(d["take_profit"])
|
||||
direction = d.get("direction", "").strip()
|
||||
if not direction:
|
||||
direction = "long" if sl < entry else "short"
|
||||
open_time = d.get("open_time", "").strip()
|
||||
lots = float(d.get("lots") or 1)
|
||||
conn = get_db()
|
||||
conn.execute(
|
||||
"""INSERT INTO position_monitors
|
||||
(symbol, symbol_name, market_code, sina_code, direction,
|
||||
lots, entry_price, stop_loss, take_profit, open_time)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
symbol, symbol_name, market_code, sina_code, direction,
|
||||
lots, entry, sl, tp, open_time,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
flash("持仓已添加")
|
||||
flash("持仓由策略交易或 CTP 自动同步,无需手工录入")
|
||||
return redirect(url_for("positions"))
|
||||
|
||||
|
||||
|
||||
+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():
|
||||
|
||||
+5
-16
@@ -1,20 +1,9 @@
|
||||
.trade-page{max-width:720px;margin:0 auto}
|
||||
.trade-page{max-width:960px;margin:0 auto}
|
||||
.trade-top-bar{display:flex;flex-wrap:wrap;gap:.65rem;align-items:center;margin-bottom:1rem}
|
||||
.trade-order-card{padding:1.25rem}
|
||||
.trade-tabs{display:flex;gap:1rem;margin-bottom:1rem;font-size:.88rem}
|
||||
.trade-tabs span.active{color:var(--accent);font-weight:600;border-bottom:2px solid var(--accent);padding-bottom:.25rem}
|
||||
.trade-tabs a{color:var(--text-muted);text-decoration:none}
|
||||
.trade-input-row,.trade-risk-row{display:grid;grid-template-columns:2fr 1fr 1fr;gap:.65rem;margin-bottom:.75rem}
|
||||
.trade-field label{display:block;font-size:.72rem;margin-bottom:.25rem;color:var(--text-label)}
|
||||
.trade-btn-row{display:grid;grid-template-columns:repeat(4,1fr);gap:.5rem;margin:1rem 0}
|
||||
.trade-btn{border:none;border-radius:8px;padding:.75rem .35rem;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:.15rem;color:#fff;font-weight:600}
|
||||
.trade-btn .btn-price{font-size:1.1rem}
|
||||
.trade-btn .btn-label{font-size:.85rem}
|
||||
.trade-btn .btn-sub{font-size:.68rem;opacity:.85;font-weight:400}
|
||||
.trade-btn.long{background:linear-gradient(180deg,#e74c3c,#c0392b)}
|
||||
.trade-btn.lock{background:linear-gradient(180deg,#27ae60,#1e8449)}
|
||||
.trade-btn.close{background:linear-gradient(180deg,#3498db,#2980b9)}
|
||||
.trade-footer{background:var(--card-inner);border-radius:8px;padding:.75rem 1rem;font-size:.82rem;line-height:1.55;border:1px solid var(--card-border)}
|
||||
.trade-subnav{display:flex;gap:1rem;margin-bottom:1rem;font-size:.88rem}
|
||||
.trade-subnav span.active{color:var(--accent);font-weight:600;border-bottom:2px solid var(--accent);padding-bottom:.25rem}
|
||||
.trade-subnav a{color:var(--text-muted);text-decoration:none}
|
||||
.trade-footer{background:var(--card-inner);border-radius:8px;padding:.75rem 1rem;font-size:.82rem;line-height:1.55;border:1px solid var(--card-border);margin-top:1rem}
|
||||
.trade-footer strong{color:var(--accent)}
|
||||
.rec-blocked td{opacity:.55}
|
||||
.rec-ok td:first-child{font-weight:600}
|
||||
|
||||
+133
-97
@@ -1,95 +1,142 @@
|
||||
(function () {
|
||||
var symInput = document.getElementById('trade-symbol');
|
||||
var lotsInput = document.getElementById('trade-lots');
|
||||
var priceInput = document.getElementById('trade-price');
|
||||
var footer = document.getElementById('trade-footer');
|
||||
var slInput = document.getElementById('trade-sl');
|
||||
var tpInput = document.getElementById('trade-tp');
|
||||
var debounceTimer;
|
||||
var list = document.getElementById('position-live-list');
|
||||
var pollTimer = null;
|
||||
|
||||
function selectedSymbol() {
|
||||
return (symInput && symInput.value || '').trim();
|
||||
function fmtNum(v, digits) {
|
||||
if (v === null || v === undefined) return '--';
|
||||
return Number(v).toFixed(digits === undefined ? 2 : digits);
|
||||
}
|
||||
|
||||
function refreshQuote() {
|
||||
var sym = selectedSymbol();
|
||||
var lots = lotsInput ? lotsInput.value : '1';
|
||||
if (!sym) return;
|
||||
fetch('/api/trade/quote?symbol=' + encodeURIComponent(sym) + '&lots=' + encodeURIComponent(lots))
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (!data.ok) return;
|
||||
if (priceInput && !priceInput.dataset.manual && data.price) {
|
||||
priceInput.value = data.price;
|
||||
}
|
||||
var px = data.price != null ? data.price : '—';
|
||||
['px-long', 'px-short'].forEach(function (id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.textContent = px;
|
||||
});
|
||||
var ml = document.getElementById('max-long');
|
||||
var ms = document.getElementById('max-short');
|
||||
if (ml) ml.textContent = '≤' + (data.max_open_long || '—');
|
||||
if (ms) ms.textContent = '≤' + (data.max_open_short || '—');
|
||||
document.getElementById('pos-long').textContent = '≤' + (data.pos_long || 0);
|
||||
document.getElementById('pos-short').textContent = '≤' + (data.pos_short || 0);
|
||||
if (footer && data.metrics) {
|
||||
var m = data.metrics;
|
||||
footer.innerHTML =
|
||||
'<p><strong>' + (data.name || sym) + '</strong> ' + (data.footer_text || '') + '</p>' +
|
||||
'<p>价格精度 <strong>' + m.price_precision + '</strong> 位 · ' +
|
||||
'最小变动 <strong>' + m.tick_size + '</strong> · ' +
|
||||
'每跳 <strong>' + m.tick_value_per_lot + '</strong> 元/手 · ' +
|
||||
'当前 <strong>' + lots + '</strong> 手每跳合计 <strong class="text-accent">' + m.tick_value_total + '</strong> 元</p>' +
|
||||
(m.margin_total ? '<p class="text-muted">预估保证金约 ' + m.margin_total + ' 元</p>' : '');
|
||||
}
|
||||
}).catch(function () {});
|
||||
function buildPosCard(row) {
|
||||
var pnlClass = '';
|
||||
if (row.float_pnl > 0) pnlClass = 'pnl-pos';
|
||||
if (row.float_pnl < 0) pnlClass = 'pnl-neg';
|
||||
var pnlText = '--';
|
||||
if (row.float_pnl != null) {
|
||||
var sign = row.float_pnl >= 0 ? '+' : '';
|
||||
pnlText = sign + fmtNum(row.float_pnl) + '元';
|
||||
if (row.float_pct != null) {
|
||||
pnlText += ' (' + sign + fmtNum(row.float_pct) + '%)';
|
||||
}
|
||||
}
|
||||
var rr = row.rr_ratio != null ? row.rr_ratio + ':1' : '--';
|
||||
var openT = (row.open_time || '').replace('T', ' ').slice(0, 16);
|
||||
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
|
||||
var closeBtn = '';
|
||||
|
||||
if (row.close_url) {
|
||||
closeBtn =
|
||||
'<form method="post" action="' + row.close_url + '" style="display:inline" onsubmit="return confirm(\'确认平仓?\')">' +
|
||||
'<button type="submit" class="btn-del pos-del">平仓</button></form>';
|
||||
} else if (row.can_close) {
|
||||
closeBtn =
|
||||
'<button type="button" class="btn-del pos-del" data-close=\'' + JSON.stringify({
|
||||
source: row.source,
|
||||
symbol_code: row.symbol_code,
|
||||
direction: row.direction,
|
||||
lots: row.lots,
|
||||
mark_price: row.mark_price,
|
||||
monitor_id: row.monitor_id || null
|
||||
}) + '\'>平仓</button>';
|
||||
}
|
||||
|
||||
var metaParts = ['来源 <strong>' + (row.source_label || row.source) + '</strong>'];
|
||||
if (row.risk_pct != null) {
|
||||
metaParts.push('风险 <strong>' + fmtNum(row.risk_pct) + '%≈' + fmtNum(row.risk_amount) + '元</strong>');
|
||||
}
|
||||
if (row.tick_value_total != null) {
|
||||
metaParts.push('每跳 <strong>' + fmtNum(row.tick_value_total) + '元</strong>');
|
||||
}
|
||||
|
||||
var slTp =
|
||||
'<div class="cell"><label>止损</label><div>' + (row.stop_loss != null ? fmtNum(row.stop_loss) : '--') + '</div></div>' +
|
||||
'<div class="cell"><label>止盈</label><div>' + (row.take_profit != null ? fmtNum(row.take_profit) : '--') + '</div></div>';
|
||||
|
||||
var footerParts = ['张数 ' + row.lots];
|
||||
if (row.margin != null) footerParts.push('保证金 ' + fmtNum(row.margin) + '元');
|
||||
if (row.position_pct != null) footerParts.push('仓位占比 ' + fmtNum(row.position_pct) + '%');
|
||||
if (openT) footerParts.push('开仓 ' + openT);
|
||||
if (row.holding_duration) footerParts.push('持仓 ' + row.holding_duration);
|
||||
if (row.est_fee != null) footerParts.push('手续费(估) ' + fmtNum(row.est_fee) + '元');
|
||||
|
||||
return (
|
||||
'<div class="pos-card" data-key="' + (row.key || '') + '">' +
|
||||
'<div class="pos-card-head">' +
|
||||
'<div><div class="title">' + row.symbol + ' <span class="badge dir">' + dirBadge + '</span></div>' +
|
||||
(row.symbol_code && row.symbol_code !== row.symbol ? '<div class="text-muted" style="font-size:.72rem;margin-top:.15rem">' + row.symbol_code + '</div>' : '') +
|
||||
'</div>' + closeBtn + '</div>' +
|
||||
'<div class="pos-card-meta">' + metaParts.join(' · ') + '</div>' +
|
||||
'<div class="pos-metrics">' +
|
||||
'<div class="cell"><label>成交价</label><div>' + fmtNum(row.entry_price) + '</div></div>' +
|
||||
slTp +
|
||||
'<div class="cell"><label>盈亏比</label><div>' + rr + '</div></div>' +
|
||||
'<div class="cell"><label>标记价</label><div>' + (row.mark_price != null ? fmtNum(row.mark_price) : '--') + '</div></div>' +
|
||||
'<div class="cell ' + pnlClass + '"><label>浮盈亏</label><div>' + pnlText + '</div></div>' +
|
||||
(row.est_fee != null ?
|
||||
'<div class="cell"><label>预估手续费</label><div>' + fmtNum(row.est_fee) + '元</div></div>' +
|
||||
'<div class="cell ' + (row.est_pnl_net > 0 ? 'pnl-pos' : (row.est_pnl_net < 0 ? 'pnl-neg' : '')) + '">' +
|
||||
'<label>扣费后</label><div>' + (row.est_pnl_net != null ? fmtNum(row.est_pnl_net) + '元' : '--') + '</div></div>'
|
||||
: '') +
|
||||
'</div>' +
|
||||
'<div class="pos-footer">' + footerParts.map(function (s) { return '<span>' + s + '</span>'; }).join('') + '</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
function scheduleRefresh() {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(refreshQuote, 400);
|
||||
}
|
||||
|
||||
if (symInput) symInput.addEventListener('input', scheduleRefresh);
|
||||
if (lotsInput) lotsInput.addEventListener('input', scheduleRefresh);
|
||||
if (priceInput) {
|
||||
priceInput.addEventListener('input', function () {
|
||||
priceInput.dataset.manual = '1';
|
||||
});
|
||||
}
|
||||
|
||||
function postOrder(offset, direction) {
|
||||
var sym = selectedSymbol();
|
||||
if (!sym) { alert('请选择品种'); return; }
|
||||
var body = {
|
||||
symbol: sym,
|
||||
offset: offset,
|
||||
direction: direction,
|
||||
lots: parseInt(lotsInput.value, 10) || 1,
|
||||
price: parseFloat(priceInput.value) || 0,
|
||||
stop_loss: slInput ? parseFloat(slInput.value) : null,
|
||||
take_profit: tpInput ? parseFloat(tpInput.value) : null
|
||||
};
|
||||
fetch('/api/trade/order', {
|
||||
function closePosition(payload) {
|
||||
var price = payload.mark_price;
|
||||
if (!price || price <= 0) {
|
||||
alert('无法获取现价,请稍后重试');
|
||||
return;
|
||||
}
|
||||
if (!confirm('确认以 ' + price + ' 限价平仓 ' + payload.lots + ' 手?')) return;
|
||||
fetch('/api/trading/close', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
}).then(function (r) { return r.json(); }).then(function (data) {
|
||||
if (!data.ok) { alert(data.error || '下单失败'); return; }
|
||||
alert('已提交 ' + (data.lots || '') + ' 手');
|
||||
location.reload();
|
||||
});
|
||||
body: JSON.stringify({
|
||||
source: payload.source,
|
||||
symbol_code: payload.symbol_code,
|
||||
direction: payload.direction,
|
||||
lots: payload.lots,
|
||||
price: price,
|
||||
monitor_id: payload.monitor_id
|
||||
})
|
||||
}).then(function (r) { return r.json(); }).then(function (d) {
|
||||
if (!d.ok) { alert(d.error || '平仓失败'); return; }
|
||||
pollPositions();
|
||||
}).catch(function () { alert('平仓请求失败'); });
|
||||
}
|
||||
|
||||
var btnLong = document.getElementById('btn-open-long');
|
||||
var btnShort = document.getElementById('btn-open-short');
|
||||
var btnCloseL = document.getElementById('btn-close-long');
|
||||
var btnCloseS = document.getElementById('btn-close-short');
|
||||
if (btnLong) btnLong.addEventListener('click', function () { postOrder('open', 'long'); });
|
||||
if (btnShort) btnShort.addEventListener('click', function () { postOrder('open', 'short'); });
|
||||
if (btnCloseL) btnCloseL.addEventListener('click', function () { postOrder('close', 'long'); });
|
||||
if (btnCloseS) btnCloseS.addEventListener('click', function () { postOrder('close', 'short'); });
|
||||
function pollPositions() {
|
||||
if (!list) return;
|
||||
fetch('/api/trading/live')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
var cap = document.getElementById('cap-display');
|
||||
if (cap && data.capital != null) cap.textContent = Number(data.capital).toFixed(2);
|
||||
var ctpBadge = document.getElementById('ctp-badge');
|
||||
if (ctpBadge && data.ctp_status) {
|
||||
ctpBadge.textContent = data.ctp_status.connected ? 'CTP 已连接' : 'CTP 未连接';
|
||||
ctpBadge.className = 'badge ' + (data.ctp_status.connected ? 'profit' : 'planned');
|
||||
}
|
||||
var rows = data.rows || [];
|
||||
if (!rows.length) {
|
||||
list.innerHTML = '<div class="empty-hint">暂无持仓。请先在「策略交易」开仓,或连接 CTP 同步柜台持仓。</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = rows.map(buildPosCard).join('');
|
||||
list.querySelectorAll('[data-close]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
closePosition(JSON.parse(btn.getAttribute('data-close')));
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
if (list.innerHTML.indexOf('pos-card') < 0) {
|
||||
list.innerHTML = '<div class="empty-hint text-loss">加载失败,请刷新页面</div>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var btnConnect = document.getElementById('btn-ctp-connect');
|
||||
if (btnConnect) {
|
||||
@@ -109,19 +156,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(function () {
|
||||
fetch('/api/account_snapshot').then(function (r) { return r.json(); }).then(function (d) {
|
||||
var cap = document.getElementById('cap-display');
|
||||
if (cap && d.capital != null) cap.textContent = Number(d.capital).toFixed(2);
|
||||
var badge = document.getElementById('risk-badge');
|
||||
if (badge && d.risk_status) badge.textContent = d.risk_status.status_label;
|
||||
var ctpBadge = document.getElementById('ctp-badge');
|
||||
if (ctpBadge && d.ctp_status) {
|
||||
ctpBadge.textContent = d.ctp_status.connected ? 'CTP 已连接' : 'CTP 未连接';
|
||||
ctpBadge.className = 'badge ' + (d.ctp_status.connected ? 'profit' : 'planned');
|
||||
}
|
||||
}).catch(function () {});
|
||||
}, 5000);
|
||||
|
||||
scheduleRefresh();
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
pollPositions();
|
||||
pollTimer = setInterval(pollPositions, 2000);
|
||||
});
|
||||
})();
|
||||
|
||||
+1
-2
@@ -483,12 +483,11 @@
|
||||
<h1 class="site-title">国内期货 · 交易监控 + 复盘<span class="site-title-sub">FUTURES MONITOR SYSTEM</span></h1>
|
||||
<button type="button" class="nav-backdrop" id="nav-backdrop" aria-label="关闭菜单" hidden></button>
|
||||
<nav class="site-nav" id="site-nav">
|
||||
<a href="{{ url_for('trade_page') }}" class="{% if request.endpoint == 'trade_page' %}active{% endif %}">期货下单</a>
|
||||
<a href="{{ url_for('positions') }}" class="{% if request.endpoint in ('positions', 'trade_page') %}active{% endif %}">持仓监控</a>
|
||||
<a href="{{ url_for('recommend_page') }}" class="{% if request.endpoint == 'recommend_page' %}active{% endif %}">品种推荐</a>
|
||||
<a href="{{ url_for('strategy_page') }}" class="{% if request.endpoint in ('strategy_page', 'strategy_records_page') %}active{% endif %}">策略交易</a>
|
||||
<a href="{{ url_for('plans') }}" class="{% if request.endpoint == 'plans' %}active{% endif %}">开单计划</a>
|
||||
<a href="{{ url_for('keys') }}" class="{% if request.endpoint == 'keys' %}active{% endif %}">关键位监控</a>
|
||||
<a href="{{ url_for('positions') }}" class="{% if request.endpoint == 'positions' %}active{% endif %}">持仓监控</a>
|
||||
<a href="{{ url_for('market_page') }}" class="{% if request.endpoint == 'market_page' %}active{% endif %}">行情K线</a>
|
||||
<a href="{{ url_for('records') }}" class="{% if request.endpoint in ('records', 'trades') %}active{% endif %}">交易记录与复盘</a>
|
||||
<a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
</form>
|
||||
<p class="hint" style="margin-top:.75rem">
|
||||
<strong>模拟盘</strong>连接上期 SimNow 仿真柜台(非本地假资金)。在 <code>.env</code> 配置
|
||||
<code>SIMNOW_USER</code>、<code>SIMNOW_PASSWORD</code> 等,在「期货下单」页点击连接 CTP。<br>
|
||||
<code>SIMNOW_USER</code>、<code>SIMNOW_PASSWORD</code> 等,在「持仓监控」页点击连接 CTP。<br>
|
||||
<strong>实盘</strong>后期配置 <code>CTP_LIVE_*</code> 对接你的期货公司。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<button type="button" class="btn-primary" id="btn-roll-exec" hidden>执行滚仓</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="empty-hint">请先在「期货下单」开仓并建立监控。</p>
|
||||
<p class="empty-hint">请先在「策略交易」开仓,持仓将自动出现在「持仓监控」。</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+16
-71
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}期货下单 - 国内期货监控系统{% endblock %}
|
||||
{% block title %}持仓监控 - 国内期货监控系统{% endblock %}
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/trade.css') }}">
|
||||
{% endblock %}
|
||||
@@ -12,85 +12,30 @@
|
||||
</span>
|
||||
<span class="badge {% if risk_status.can_trade %}profit{% else %}loss{% endif %}" id="risk-badge">{{ risk_status.status_label }}</span>
|
||||
<span class="text-muted">权益 <strong id="cap-display">{{ '%.2f'|format(capital) }}</strong> 元</span>
|
||||
{% if ctp_account.available is defined and ctp_status.connected %}
|
||||
<span class="text-muted">可用 <strong>{{ '%.2f'|format(ctp_account.available) }}</strong> 元</span>
|
||||
{% endif %}
|
||||
<button type="button" class="btn-primary" id="btn-ctp-connect" style="padding:.4rem .9rem;font-size:.8rem">连接 CTP</button>
|
||||
</div>
|
||||
|
||||
<div class="card trade-order-card">
|
||||
<div class="trade-tabs">
|
||||
<span class="active">期货下单</span>
|
||||
<a href="{{ url_for('recommend_page') }}">品种推荐</a>
|
||||
<a href="{{ url_for('strategy_page') }}">策略交易</a>
|
||||
</div>
|
||||
|
||||
<div class="trade-input-row">
|
||||
<div class="symbol-wrap trade-field">
|
||||
<label class="text-label">品种</label>
|
||||
<input type="text" id="trade-symbol" class="symbol-input" placeholder="主力合约 rb2610" autocomplete="off" value="">
|
||||
<div class="symbol-dropdown"></div>
|
||||
<div class="symbol-selected" id="sym-selected"></div>
|
||||
</div>
|
||||
<div class="trade-field">
|
||||
<label class="text-label">手数</label>
|
||||
<input type="number" id="trade-lots" min="1" step="1" value="1">
|
||||
</div>
|
||||
<div class="trade-field">
|
||||
<label class="text-label">价格</label>
|
||||
<input type="number" id="trade-price" step="any" placeholder="限价">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="risk-fields" class="trade-risk-row" {% if sizing_mode != 'risk' %}hidden{% endif %}>
|
||||
<div class="trade-field"><label class="text-label">止损</label><input type="number" id="trade-sl" step="any"></div>
|
||||
<div class="trade-field"><label class="text-label">止盈</label><input type="number" id="trade-tp" step="any"></div>
|
||||
</div>
|
||||
|
||||
<div class="trade-btn-row">
|
||||
<button type="button" class="trade-btn long" id="btn-open-long">
|
||||
<span class="btn-price" id="px-long">—</span>
|
||||
<span class="btn-label">加多</span>
|
||||
<span class="btn-sub" id="max-long">≤—</span>
|
||||
</button>
|
||||
<button type="button" class="trade-btn lock" id="btn-open-short">
|
||||
<span class="btn-price" id="px-short">—</span>
|
||||
<span class="btn-label">加空</span>
|
||||
<span class="btn-sub" id="max-short">≤—</span>
|
||||
</button>
|
||||
<button type="button" class="trade-btn close" id="btn-close-long">
|
||||
<span class="btn-sub">平多</span>
|
||||
<span class="btn-label">平多</span>
|
||||
<span class="btn-sub" id="pos-long">≤0</span>
|
||||
</button>
|
||||
<button type="button" class="trade-btn close" id="btn-close-short">
|
||||
<span class="btn-sub">平空</span>
|
||||
<span class="btn-label">平空</span>
|
||||
<span class="btn-sub" id="pos-short">≤0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="trade-footer" id="trade-footer">
|
||||
<p class="hint">SimNow 模拟盘:请先连接 CTP。输入品种与手数后显示跳动价值与价格精度。</p>
|
||||
{% if ctp_status.last_error %}<p class="text-loss" style="font-size:.78rem">{{ ctp_status.last_error }}</p>{% endif %}
|
||||
</div>
|
||||
<div class="trade-subnav">
|
||||
<span class="active">持仓监控</span>
|
||||
<a href="{{ url_for('recommend_page') }}">品种推荐</a>
|
||||
<a href="{{ url_for('strategy_page') }}">策略交易</a>
|
||||
</div>
|
||||
|
||||
{% if ctp_positions %}
|
||||
<div class="card">
|
||||
<h2>CTP 持仓(SimNow / 柜台)</h2>
|
||||
<ul class="list">
|
||||
{% for p in ctp_positions %}
|
||||
<li class="list-item">
|
||||
<span>{{ p.symbol }} {{ '多' if p.direction=='long' else '空' }} {{ p.lots }}手 @ {{ p.avg_price }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<h2>实时持仓</h2>
|
||||
<div class="card-body" id="position-live-list">
|
||||
<div class="empty-hint">加载中…</div>
|
||||
</div>
|
||||
<div class="trade-footer" id="trade-footer">
|
||||
<p class="hint">开仓请使用「策略交易」;连接 CTP 后自动同步 SimNow / 柜台持仓与程序监控。</p>
|
||||
{% if ctp_status.last_error %}<p class="text-loss" style="font-size:.78rem;margin-top:.5rem">{{ ctp_status.last_error }}</p>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
window.TRADE_SIZING_MODE = {{ sizing_mode|tojson }};
|
||||
window.TRADE_RISK_PERCENT = {{ risk_percent }};
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/trade.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user