合并期货下单与持仓监控为统一界面,移除手工录入。
策略与 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)
|
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"])
|
@app.route("/add_key", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -928,36 +918,7 @@ def add_key():
|
|||||||
@app.route("/add_position", methods=["POST"])
|
@app.route("/add_position", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def add_position():
|
def add_position():
|
||||||
d = request.form
|
flash("持仓由策略交易或 CTP 自动同步,无需手工录入")
|
||||||
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("持仓已添加")
|
|
||||||
return redirect(url_for("positions"))
|
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 flask import flash, jsonify, redirect, render_template, request, url_for
|
||||||
|
|
||||||
from contract_specs import calc_position_metrics, get_contract_spec
|
from contract_specs import calc_position_metrics, get_contract_spec
|
||||||
|
from fee_specs import calc_fee_breakdown
|
||||||
from position_sizing import (
|
from position_sizing import (
|
||||||
MODE_FIXED,
|
MODE_FIXED,
|
||||||
MODE_RISK,
|
MODE_RISK,
|
||||||
@@ -98,32 +99,264 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
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")
|
@app.route("/trade")
|
||||||
@login_required
|
@login_required
|
||||||
def trade_page():
|
def trade_page():
|
||||||
|
return redirect(url_for("positions"))
|
||||||
|
|
||||||
|
@app.route("/positions")
|
||||||
|
@login_required
|
||||||
|
def positions():
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
init_strategy_tables(conn)
|
init_strategy_tables(conn)
|
||||||
mode = get_trading_mode(get_setting)
|
mode = get_trading_mode(get_setting)
|
||||||
ctp_st = ctp_status(mode)
|
ctp_st = ctp_status(mode)
|
||||||
capital = _capital(conn)
|
capital = _capital(conn)
|
||||||
sizing = get_sizing_mode(get_setting)
|
|
||||||
risk = get_risk_status(conn)
|
risk = get_risk_status(conn)
|
||||||
ctp_acc = _ctp_account(mode) if ctp_st.get("connected") else {}
|
ctp_acc = _ctp_account(mode) if ctp_st.get("connected") else {}
|
||||||
positions = _ctp_positions(mode) if ctp_st.get("connected") else []
|
|
||||||
conn.close()
|
conn.close()
|
||||||
return render_template(
|
return render_template(
|
||||||
"trade.html",
|
"trade.html",
|
||||||
trading_mode=mode,
|
trading_mode=mode,
|
||||||
trading_mode_label=trading_mode_label(get_setting),
|
trading_mode_label=trading_mode_label(get_setting),
|
||||||
sizing_mode=sizing,
|
|
||||||
risk_percent=get_risk_percent(get_setting),
|
|
||||||
capital=capital,
|
capital=capital,
|
||||||
risk_status=risk,
|
risk_status=risk,
|
||||||
ctp_status=ctp_st,
|
ctp_status=ctp_st,
|
||||||
ctp_account=ctp_acc,
|
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")
|
@app.route("/recommend")
|
||||||
@login_required
|
@login_required
|
||||||
def recommend_page():
|
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-top-bar{display:flex;flex-wrap:wrap;gap:.65rem;align-items:center;margin-bottom:1rem}
|
||||||
.trade-order-card{padding:1.25rem}
|
.trade-subnav{display:flex;gap:1rem;margin-bottom:1rem;font-size:.88rem}
|
||||||
.trade-tabs{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-tabs 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-tabs 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-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-footer strong{color:var(--accent)}
|
.trade-footer strong{color:var(--accent)}
|
||||||
.rec-blocked td{opacity:.55}
|
.rec-blocked td{opacity:.55}
|
||||||
.rec-ok td:first-child{font-weight:600}
|
.rec-ok td:first-child{font-weight:600}
|
||||||
|
|||||||
+127
-91
@@ -1,95 +1,142 @@
|
|||||||
(function () {
|
(function () {
|
||||||
var symInput = document.getElementById('trade-symbol');
|
var list = document.getElementById('position-live-list');
|
||||||
var lotsInput = document.getElementById('trade-lots');
|
var pollTimer = null;
|
||||||
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;
|
|
||||||
|
|
||||||
function selectedSymbol() {
|
function fmtNum(v, digits) {
|
||||||
return (symInput && symInput.value || '').trim();
|
if (v === null || v === undefined) return '--';
|
||||||
|
return Number(v).toFixed(digits === undefined ? 2 : digits);
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshQuote() {
|
function buildPosCard(row) {
|
||||||
var sym = selectedSymbol();
|
var pnlClass = '';
|
||||||
var lots = lotsInput ? lotsInput.value : '1';
|
if (row.float_pnl > 0) pnlClass = 'pnl-pos';
|
||||||
if (!sym) return;
|
if (row.float_pnl < 0) pnlClass = 'pnl-neg';
|
||||||
fetch('/api/trade/quote?symbol=' + encodeURIComponent(sym) + '&lots=' + encodeURIComponent(lots))
|
var pnlText = '--';
|
||||||
.then(function (r) { return r.json(); })
|
if (row.float_pnl != null) {
|
||||||
.then(function (data) {
|
var sign = row.float_pnl >= 0 ? '+' : '';
|
||||||
if (!data.ok) return;
|
pnlText = sign + fmtNum(row.float_pnl) + '元';
|
||||||
if (priceInput && !priceInput.dataset.manual && data.price) {
|
if (row.float_pct != null) {
|
||||||
priceInput.value = data.price;
|
pnlText += ' (' + sign + fmtNum(row.float_pct) + '%)';
|
||||||
}
|
}
|
||||||
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 () {});
|
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>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleRefresh() {
|
var metaParts = ['来源 <strong>' + (row.source_label || row.source) + '</strong>'];
|
||||||
clearTimeout(debounceTimer);
|
if (row.risk_pct != null) {
|
||||||
debounceTimer = setTimeout(refreshQuote, 400);
|
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>');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (symInput) symInput.addEventListener('input', scheduleRefresh);
|
var slTp =
|
||||||
if (lotsInput) lotsInput.addEventListener('input', scheduleRefresh);
|
'<div class="cell"><label>止损</label><div>' + (row.stop_loss != null ? fmtNum(row.stop_loss) : '--') + '</div></div>' +
|
||||||
if (priceInput) {
|
'<div class="cell"><label>止盈</label><div>' + (row.take_profit != null ? fmtNum(row.take_profit) : '--') + '</div></div>';
|
||||||
priceInput.addEventListener('input', function () {
|
|
||||||
priceInput.dataset.manual = '1';
|
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 postOrder(offset, direction) {
|
function closePosition(payload) {
|
||||||
var sym = selectedSymbol();
|
var price = payload.mark_price;
|
||||||
if (!sym) { alert('请选择品种'); return; }
|
if (!price || price <= 0) {
|
||||||
var body = {
|
alert('无法获取现价,请稍后重试');
|
||||||
symbol: sym,
|
return;
|
||||||
offset: offset,
|
}
|
||||||
direction: direction,
|
if (!confirm('确认以 ' + price + ' 限价平仓 ' + payload.lots + ' 手?')) return;
|
||||||
lots: parseInt(lotsInput.value, 10) || 1,
|
fetch('/api/trading/close', {
|
||||||
price: parseFloat(priceInput.value) || 0,
|
|
||||||
stop_loss: slInput ? parseFloat(slInput.value) : null,
|
|
||||||
take_profit: tpInput ? parseFloat(tpInput.value) : null
|
|
||||||
};
|
|
||||||
fetch('/api/trade/order', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify({
|
||||||
}).then(function (r) { return r.json(); }).then(function (data) {
|
source: payload.source,
|
||||||
if (!data.ok) { alert(data.error || '下单失败'); return; }
|
symbol_code: payload.symbol_code,
|
||||||
alert('已提交 ' + (data.lots || '') + ' 手');
|
direction: payload.direction,
|
||||||
location.reload();
|
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');
|
function pollPositions() {
|
||||||
var btnShort = document.getElementById('btn-open-short');
|
if (!list) return;
|
||||||
var btnCloseL = document.getElementById('btn-close-long');
|
fetch('/api/trading/live')
|
||||||
var btnCloseS = document.getElementById('btn-close-short');
|
.then(function (r) { return r.json(); })
|
||||||
if (btnLong) btnLong.addEventListener('click', function () { postOrder('open', 'long'); });
|
.then(function (data) {
|
||||||
if (btnShort) btnShort.addEventListener('click', function () { postOrder('open', 'short'); });
|
var cap = document.getElementById('cap-display');
|
||||||
if (btnCloseL) btnCloseL.addEventListener('click', function () { postOrder('close', 'long'); });
|
if (cap && data.capital != null) cap.textContent = Number(data.capital).toFixed(2);
|
||||||
if (btnCloseS) btnCloseS.addEventListener('click', function () { postOrder('close', 'short'); });
|
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');
|
var btnConnect = document.getElementById('btn-ctp-connect');
|
||||||
if (btnConnect) {
|
if (btnConnect) {
|
||||||
@@ -109,19 +156,8 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
fetch('/api/account_snapshot').then(function (r) { return r.json(); }).then(function (d) {
|
pollPositions();
|
||||||
var cap = document.getElementById('cap-display');
|
pollTimer = setInterval(pollPositions, 2000);
|
||||||
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();
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
+1
-2
@@ -483,12 +483,11 @@
|
|||||||
<h1 class="site-title">国内期货 · 交易监控 + 复盘<span class="site-title-sub">FUTURES MONITOR SYSTEM</span></h1>
|
<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>
|
<button type="button" class="nav-backdrop" id="nav-backdrop" aria-label="关闭菜单" hidden></button>
|
||||||
<nav class="site-nav" id="site-nav">
|
<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('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('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('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('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('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('records') }}" class="{% if request.endpoint in ('records', 'trades') %}active{% endif %}">交易记录与复盘</a>
|
||||||
<a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a>
|
<a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
</form>
|
</form>
|
||||||
<p class="hint" style="margin-top:.75rem">
|
<p class="hint" style="margin-top:.75rem">
|
||||||
<strong>模拟盘</strong>连接上期 SimNow 仿真柜台(非本地假资金)。在 <code>.env</code> 配置
|
<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> 对接你的期货公司。
|
<strong>实盘</strong>后期配置 <code>CTP_LIVE_*</code> 对接你的期货公司。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
<button type="button" class="btn-primary" id="btn-roll-exec" hidden>执行滚仓</button>
|
<button type="button" class="btn-primary" id="btn-roll-exec" hidden>执行滚仓</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="empty-hint">请先在「期货下单」开仓并建立监控。</p>
|
<p class="empty-hint">请先在「策略交易」开仓,持仓将自动出现在「持仓监控」。</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+14
-69
@@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}期货下单 - 国内期货监控系统{% endblock %}
|
{% block title %}持仓监控 - 国内期货监控系统{% endblock %}
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/trade.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/trade.css') }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -12,85 +12,30 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="badge {% if risk_status.can_trade %}profit{% else %}loss{% endif %}" id="risk-badge">{{ risk_status.status_label }}</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>
|
<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>
|
<button type="button" class="btn-primary" id="btn-ctp-connect" style="padding:.4rem .9rem;font-size:.8rem">连接 CTP</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card trade-order-card">
|
<div class="trade-subnav">
|
||||||
<div class="trade-tabs">
|
<span class="active">持仓监控</span>
|
||||||
<span class="active">期货下单</span>
|
|
||||||
<a href="{{ url_for('recommend_page') }}">品种推荐</a>
|
<a href="{{ url_for('recommend_page') }}">品种推荐</a>
|
||||||
<a href="{{ url_for('strategy_page') }}">策略交易</a>
|
<a href="{{ url_for('strategy_page') }}">策略交易</a>
|
||||||
</div>
|
</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>
|
|
||||||
|
|
||||||
{% if ctp_positions %}
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>CTP 持仓(SimNow / 柜台)</h2>
|
<h2>实时持仓</h2>
|
||||||
<ul class="list">
|
<div class="card-body" id="position-live-list">
|
||||||
{% for p in ctp_positions %}
|
<div class="empty-hint">加载中…</div>
|
||||||
<li class="list-item">
|
</div>
|
||||||
<span>{{ p.symbol }} {{ '多' if p.direction=='long' else '空' }} {{ p.lots }}手 @ {{ p.avg_price }}</span>
|
<div class="trade-footer" id="trade-footer">
|
||||||
</li>
|
<p class="hint">开仓请使用「策略交易」;连接 CTP 后自动同步 SimNow / 柜台持仓与程序监控。</p>
|
||||||
{% endfor %}
|
{% if ctp_status.last_error %}<p class="text-loss" style="font-size:.78rem;margin-top:.5rem">{{ ctp_status.last_error }}</p>{% endif %}
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block extra_js %}
|
{% 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>
|
<script src="{{ url_for('static', filename='js/trade.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user