Enhance dashboard with exchange labels, split SL/TP columns, and daily risk limits.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-29 22:04:44 +08:00
parent b460c6c4e5
commit d8c6428eb5
7 changed files with 303 additions and 54 deletions
+3
View File
@@ -50,3 +50,6 @@ RISK_COOLING_HOURS_MANUAL=4
RISK_COOLING_HOURS_MANUAL_JOURNAL=1 RISK_COOLING_HOURS_MANUAL_JOURNAL=1
RISK_MANUAL_CLOSE_DAILY_LIMIT=2 RISK_MANUAL_CLOSE_DAILY_LIMIT=2
MAX_ACTIVE_POSITIONS=1 MAX_ACTIVE_POSITIONS=1
RISK_DAILY_POSITION_LIMIT=5
RISK_DAILY_TRADING_RISK_PCT=2
TRADING_DAY_RESET_HOUR=8
+23 -9
View File
@@ -37,13 +37,17 @@ def build_risk_overview(
margin_used: Optional[float] = None, margin_used: Optional[float] = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
from risk.account_risk_lib import ( from risk.account_risk_lib import (
cooling_hours_manual,
cooling_hours_manual_journal,
count_daily_opens,
daily_position_limit,
daily_trading_risk_pct_limit,
daily_trading_risk_used_pct,
ensure_account_risk_schema, ensure_account_risk_schema,
get_risk_status, get_risk_status,
manual_close_daily_limit, manual_close_daily_limit,
max_active_positions, max_active_positions,
risk_control_enabled, risk_control_enabled,
cooling_hours_manual,
cooling_hours_manual_journal,
trading_day_label, trading_day_label,
trading_day_reset_hour, trading_day_reset_hour,
) )
@@ -52,12 +56,11 @@ def build_risk_overview(
get_fixed_lots, get_fixed_lots,
get_max_margin_pct, get_max_margin_pct,
get_roll_max_margin_pct, get_roll_max_margin_pct,
get_risk_percent,
get_sizing_mode, get_sizing_mode,
) )
ensure_account_risk_schema(conn) ensure_account_risk_schema(conn)
risk = dict(get_risk_status(conn) or {}) risk = dict(get_risk_status(conn, equity=equity) or {})
row = conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone() row = conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone()
td = trading_day_label() td = trading_day_label()
stored_td = str(row["trading_day"] or "") if row else "" stored_td = str(row["trading_day"] or "") if row else ""
@@ -71,20 +74,28 @@ def build_risk_overview(
sizing = get_sizing_mode(get_setting) sizing = get_sizing_mode(get_setting)
sizing_label = "固定金额" if sizing == "amount" else "固定手数" sizing_label = "固定金额" if sizing == "amount" else "固定手数"
daily_opens = int(risk.get("daily_open_count") or count_daily_opens(conn))
daily_risk_used = risk.get("daily_risk_used_pct")
if daily_risk_used is None and equity and equity > 0:
daily_risk_used = daily_trading_risk_used_pct(conn, float(equity))
return { return {
"enabled": risk_control_enabled(), "enabled": risk_control_enabled(),
"status": risk, "status": risk,
"manual_close_count_today": manual_count, "manual_close_count_today": manual_count,
"margin_pct_used": margin_pct_used, "margin_pct_used": margin_pct_used,
"daily_open_count": daily_opens,
"daily_risk_used_pct": daily_risk_used,
"limits": { "limits": {
"max_active_positions": max_active_positions(), "max_active_positions": max_active_positions(),
"daily_position_limit": daily_position_limit(),
"daily_trading_risk_pct_limit": daily_trading_risk_pct_limit(),
"manual_close_daily_limit": manual_close_daily_limit(), "manual_close_daily_limit": manual_close_daily_limit(),
"cooling_hours_manual": cooling_hours_manual(), "cooling_hours_manual": cooling_hours_manual(),
"cooling_hours_manual_journal": cooling_hours_manual_journal(), "cooling_hours_manual_journal": cooling_hours_manual_journal(),
"trading_day_reset_hour": trading_day_reset_hour(), "trading_day_reset_hour": trading_day_reset_hour(),
"max_margin_pct": max_margin, "max_margin_pct": max_margin,
"roll_max_margin_pct": get_roll_max_margin_pct(get_setting), "roll_max_margin_pct": get_roll_max_margin_pct(get_setting),
"risk_percent": get_risk_percent(get_setting),
"sizing_mode": sizing, "sizing_mode": sizing,
"sizing_label": sizing_label, "sizing_label": sizing_label,
"fixed_lots": get_fixed_lots(get_setting), "fixed_lots": get_fixed_lots(get_setting),
@@ -167,11 +178,12 @@ def build_dashboard_payload(
dist_upper = round(upper - float(price), 2) dist_upper = round(upper - float(price), 2)
dist_lower = round(float(price) - lower, 2) dist_lower = round(float(price) - lower, 2)
mtype = r["monitor_type"] or "" mtype = r["monitor_type"] or ""
sf = _symbol_fields(sym)
keys.append({ keys.append({
"id": r["id"], "id": r["id"],
"symbol": sym, "symbol": sym,
"symbol_name": r["symbol_name"] or sym, **sf,
**_symbol_fields(sym), "symbol_name": r["symbol_name"] or sf.get("symbol_name") or sym,
"monitor_type": mtype, "monitor_type": mtype,
"direction": r["direction"] or "", "direction": r["direction"] or "",
"direction_label": _direction_label(r["direction"] or "long") "direction_label": _direction_label(r["direction"] or "long")
@@ -200,11 +212,13 @@ def build_dashboard_payload(
closes: list[dict[str, Any]] = [] closes: list[dict[str, Any]] = []
for r in close_rows: for r in close_rows:
sym_code = r["symbol"] or "" sym_code = r["symbol"] or ""
sf = _symbol_fields(sym_code)
closes.append({ closes.append({
"id": r["id"], "id": r["id"],
"symbol": r["symbol_name"] or sym_code, "symbol": r["symbol_name"] or sf.get("symbol_name") or sym_code,
"symbol_code": sym_code, "symbol_code": sym_code,
**_symbol_fields(sym_code), **sf,
"symbol_name": r["symbol_name"] or sf.get("symbol_name") or sym_code,
"direction": r["direction"] or "long", "direction": r["direction"] or "long",
"direction_label": _direction_label(r["direction"] or "long"), "direction_label": _direction_label(r["direction"] or "long"),
"lots": float(r["lots"] or 0), "lots": float(r["lots"] or 0),
+27 -7
View File
@@ -1564,7 +1564,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
rows = _apply_account_margin_to_rows(rows, mode, capital) rows = _apply_account_margin_to_rows(rows, mode, capital)
_persist_ctp_snapshot_to_monitors(conn, rows, mode) _persist_ctp_snapshot_to_monitors(conn, rows, mode)
pending_orders = _build_pending_orders(conn, mode) pending_orders = _build_pending_orders(conn, mode)
risk = get_risk_status(conn, active_count=_effective_active_position_count(conn, mode)) risk = get_risk_status(
conn,
active_count=_effective_active_position_count(conn, mode),
equity=capital,
)
return { return {
"ok": True, "ok": True,
"rows": rows, "rows": rows,
@@ -1753,7 +1757,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
ctp_st = ctp_status(mode) ctp_st = ctp_status(mode)
capital = _capital(conn) capital = _capital(conn)
recommend_capital = _recommend_capital(conn) recommend_capital = _recommend_capital(conn)
risk = get_risk_status(conn, active_count=_effective_active_position_count(conn, mode)) risk = get_risk_status(
conn,
active_count=_effective_active_position_count(conn, mode),
equity=capital,
)
ctp_acc = _ctp_account(mode) if ctp_st.get("connected") else {} ctp_acc = _ctp_account(mode) if ctp_st.get("connected") else {}
active_trend = conn.execute( active_trend = conn.execute(
"SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC LIMIT 1" "SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC LIMIT 1"
@@ -2387,7 +2395,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
if d.get("trailing_be") and not d.get("stop_loss"): if d.get("trailing_be") and not d.get("stop_loss"):
conn.close() conn.close()
return jsonify({"ok": False, "error": "开启移动保本须填写止损价"}), 400 return jsonify({"ok": False, "error": "开启移动保本须填写止损价"}), 400
err = assert_can_open(conn, active_count=_effective_active_position_count(conn, mode)) err = assert_can_open(
conn,
active_count=_effective_active_position_count(conn, mode),
equity=_capital(conn),
)
if err: if err:
conn.close() conn.close()
return jsonify({"ok": False, "error": err}), 403 return jsonify({"ok": False, "error": err}), 403
@@ -2675,7 +2687,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
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)
risk = get_risk_status(conn, active_count=_effective_active_position_count(conn, mode)) risk = get_risk_status(
conn,
active_count=_effective_active_position_count(conn, mode),
equity=capital,
)
conn.commit() conn.commit()
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 [] positions = _ctp_positions(mode) if ctp_st.get("connected") else []
@@ -2800,11 +2816,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
sym = (d.get("symbol") or "").strip() sym = (d.get("symbol") or "").strip()
conn = get_db() conn = get_db()
init_strategy_tables(conn) init_strategy_tables(conn)
err = assert_can_open(conn) capital = _capital(conn)
err = assert_can_open(conn, equity=capital)
if err: if err:
conn.close() conn.close()
return jsonify({"ok": False, "error": err}), 403 return jsonify({"ok": False, "error": err}), 403
capital = _capital(conn)
scope_err = assert_product_allowed_for_capital( scope_err = assert_product_allowed_for_capital(
sym, capital, ctp_connected=is_ctp_connected(get_setting), sym, capital, ctp_connected=is_ctp_connected(get_setting),
) )
@@ -3405,7 +3421,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
sl, tp = calc_breakout_sl_tp( sl, tp = calc_breakout_sl_tp(
sym=sym, direction=direction, entry=entry, bar=bar, risk_reward=rr, sym=sym, direction=direction, entry=entry, bar=bar, risk_reward=rr,
) )
err = assert_can_open(conn, active_count=_effective_active_position_count(conn, mode)) err = assert_can_open(
conn,
active_count=_effective_active_position_count(conn, mode),
equity=_capital(conn),
)
if err: if err:
_notify(False, err, entry=entry, sl=sl, tp=tp, lots=0) _notify(False, err, entry=entry, sl=sl, tp=tp, lots=0)
return False, err return False, err
+155 -11
View File
@@ -78,6 +78,22 @@ def max_active_positions() -> int:
return 1 return 1
def daily_position_limit() -> int:
"""当日最多开仓次数(含已平)。"""
try:
return max(1, int(os.getenv("RISK_DAILY_POSITION_LIMIT", "5")))
except (TypeError, ValueError):
return 5
def daily_trading_risk_pct_limit() -> float:
"""当日累计止损风险占权益上限(%)。"""
try:
return max(0.1, float(os.getenv("RISK_DAILY_TRADING_RISK_PCT", "2")))
except (TypeError, ValueError):
return 2.0
def trading_day_reset_hour() -> int: def trading_day_reset_hour() -> int:
try: try:
return max(0, min(23, int(os.getenv("TRADING_DAY_RESET_HOUR", "8")))) return max(0, min(23, int(os.getenv("TRADING_DAY_RESET_HOUR", "8"))))
@@ -151,6 +167,90 @@ def trading_day_label(now: Optional[datetime] = None) -> str:
return dt.date().isoformat() return dt.date().isoformat()
def trading_day_start(now: Optional[datetime] = None) -> datetime:
dt = now or datetime.now(_app_tz())
if dt.tzinfo is None:
dt = dt.replace(tzinfo=_app_tz())
reset_h = trading_day_reset_hour()
start = dt.replace(hour=reset_h, minute=0, second=0, microsecond=0)
if dt.hour < reset_h:
from datetime import timedelta
start = start - timedelta(days=1)
return start
def _parse_open_time_ms(open_time: str) -> Optional[int]:
s = (open_time or "").strip().replace("T", " ")[:19]
if not s:
return None
try:
dt = datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
if dt.tzinfo is None:
dt = dt.replace(tzinfo=_app_tz())
return int(dt.timestamp() * 1000)
except ValueError:
try:
dt = datetime.strptime(s[:10], "%Y-%m-%d").replace(tzinfo=_app_tz())
return int(dt.timestamp() * 1000)
except ValueError:
return None
def _opened_in_trading_day(open_time: str, now: Optional[datetime] = None) -> bool:
oms = _parse_open_time_ms(open_time)
if oms is None:
return False
return oms >= int(trading_day_start(now).timestamp() * 1000)
def count_daily_opens(conn, now: Optional[datetime] = None) -> int:
rows = conn.execute(
"SELECT open_time FROM trade_order_monitors "
"WHERE open_time IS NOT NULL AND trim(open_time) <> ''"
).fetchall()
return sum(1 for r in rows if _opened_in_trading_day(r["open_time"], now))
def daily_trading_risk_used_pct(
conn, equity: float, now: Optional[datetime] = None,
) -> Optional[float]:
if equity <= 0:
return None
from contract_specs import calc_position_metrics
total = 0.0
rows = conn.execute(
"""SELECT symbol, direction, lots, entry_price, stop_loss, take_profit, open_time
FROM trade_order_monitors
WHERE open_time IS NOT NULL AND trim(open_time) <> ''"""
).fetchall()
for r in rows:
if not _opened_in_trading_day(r["open_time"], now):
continue
entry = float(r["entry_price"] or 0)
if entry <= 0:
continue
sl = float(r["stop_loss"] if r["stop_loss"] is not None else entry)
tp = float(r["take_profit"] if r["take_profit"] is not None else entry)
lots = int(r["lots"] or 0)
if lots <= 0:
continue
m = calc_position_metrics(
r["direction"] or "long",
entry,
sl,
tp,
lots,
entry,
equity,
r["symbol"] or "",
)
total += float(m.get("risk_amount") or 0)
if total <= 0:
return 0.0
return round(total / equity * 100, 2)
def count_active_trade_monitors(conn) -> int: def count_active_trade_monitors(conn) -> int:
try: try:
n = conn.execute( n = conn.execute(
@@ -232,7 +332,13 @@ def reduce_cooloff_after_journal(conn, *, trading_day: str, now: Optional[dateti
) )
def get_risk_status(conn, *, now: Optional[datetime] = None, active_count: Optional[int] = None) -> dict: def get_risk_status(
conn,
*,
now: Optional[datetime] = None,
active_count: Optional[int] = None,
equity: Optional[float] = None,
) -> dict:
def _load() -> dict: def _load() -> dict:
ensure_account_risk_schema(conn) ensure_account_risk_schema(conn)
row = conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone() row = conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone()
@@ -252,56 +358,94 @@ def get_risk_status(conn, *, now: Optional[datetime] = None, active_count: Optio
active = count_active_trade_monitors(conn) if active_count is None else int(active_count) active = count_active_trade_monitors(conn) if active_count is None else int(active_count)
mx = max_active_positions() mx = max_active_positions()
pos_limit = active >= mx pos_limit = active >= mx
daily_opens = count_daily_opens(conn, now)
daily_pos_lim = daily_position_limit()
daily_open_limit = daily_opens >= daily_pos_lim
daily_risk_used: Optional[float] = None
daily_risk_lim = daily_trading_risk_pct_limit()
daily_risk_limit_hit = False
if equity and float(equity) > 0:
daily_risk_used = daily_trading_risk_used_pct(conn, float(equity), now)
if daily_risk_used is not None and daily_risk_used >= daily_risk_lim:
daily_risk_limit_hit = True
base = {
"active_count": active,
"max_active_positions": mx,
"daily_open_count": daily_opens,
"daily_position_limit": daily_pos_lim,
"daily_risk_used_pct": daily_risk_used,
"daily_trading_risk_pct_limit": daily_risk_lim,
}
if daily: if daily:
return { return {
**base,
"status": STATUS_DAILY, "status": STATUS_DAILY,
"status_label": STATUS_LABELS[STATUS_DAILY], "status_label": STATUS_LABELS[STATUS_DAILY],
"can_trade": False, "can_trade": False,
"can_roll": False, "can_roll": False,
"reason": "当日日冻结,禁止新开仓", "reason": "当日日冻结,禁止新开仓",
"active_count": active,
"max_active_positions": mx,
} }
if until and int(until) > now_ms: if until and int(until) > now_ms:
rem = int((int(until) - now_ms) / 1000) rem = int((int(until) - now_ms) / 1000)
hours = float(_row_get(row, "cooloff_hours") or cooling_hours_manual()) hours = float(_row_get(row, "cooloff_hours") or cooling_hours_manual())
st = STATUS_FREEZE_1H if hours <= cooling_hours_manual_journal() + 0.01 else STATUS_FREEZE_4H st = STATUS_FREEZE_1H if hours <= cooling_hours_manual_journal() + 0.01 else STATUS_FREEZE_4H
return { return {
**base,
"status": st, "status": st,
"status_label": STATUS_LABELS[st], "status_label": STATUS_LABELS[st],
"can_trade": False, "can_trade": False,
"can_roll": pos_limit, "can_roll": pos_limit,
"reason": f"冷静期中,剩余约 {rem // 3600}h {(rem % 3600) // 60}m", "reason": f"冷静期中,剩余约 {rem // 3600}h {(rem % 3600) // 60}m",
"freeze_remaining_sec": rem, "freeze_remaining_sec": rem,
"active_count": active, }
"max_active_positions": mx, if daily_risk_limit_hit:
return {
**base,
"status": STATUS_DAILY,
"status_label": STATUS_LABELS[STATUS_DAILY],
"can_trade": False,
"can_roll": pos_limit,
"reason": f"已达日交易风险上限 {daily_risk_used:.2f}%/{daily_risk_lim:.2f}%",
}
if daily_open_limit:
return {
**base,
"status": STATUS_DAILY,
"status_label": STATUS_LABELS[STATUS_DAILY],
"can_trade": False,
"can_roll": pos_limit,
"reason": f"已达日持仓上限 {daily_opens}/{daily_pos_lim}",
} }
if pos_limit: if pos_limit:
return { return {
**base,
"status": STATUS_FREEZE_POSITION, "status": STATUS_FREEZE_POSITION,
"status_label": STATUS_LABELS[STATUS_FREEZE_POSITION], "status_label": STATUS_LABELS[STATUS_FREEZE_POSITION],
"can_trade": False, "can_trade": False,
"can_roll": True, "can_roll": True,
"reason": f"已达仓位上限 {active}/{mx}", "reason": f"已达仓位上限 {active}/{mx}",
"active_count": active,
"max_active_positions": mx,
} }
return { return {
**base,
"status": STATUS_NORMAL, "status": STATUS_NORMAL,
"status_label": STATUS_LABELS[STATUS_NORMAL], "status_label": STATUS_LABELS[STATUS_NORMAL],
"can_trade": True, "can_trade": True,
"can_roll": True, "can_roll": True,
"reason": "可新开仓", "reason": "可新开仓",
"active_count": active,
"max_active_positions": mx,
} }
return _db_retry(_load) return _db_retry(_load)
def assert_can_open(conn, *, active_count: Optional[int] = None) -> Optional[str]: def assert_can_open(
rs = get_risk_status(conn, active_count=active_count) conn,
*,
active_count: Optional[int] = None,
equity: Optional[float] = None,
) -> Optional[str]:
rs = get_risk_status(conn, active_count=active_count, equity=equity)
if not rs.get("can_trade"): if not rs.get("can_trade"):
return rs.get("reason") or "当前不可开仓" return rs.get("reason") or "当前不可开仓"
return None return None
+38 -5
View File
@@ -66,7 +66,41 @@
} }
.dashboard-risk-grid { .dashboard-risk-grid {
display: flex;
flex-wrap: wrap;
align-items: stretch;
gap: 0;
margin-bottom: 0; margin-bottom: 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.dashboard-risk-item {
flex: 1 1 0;
min-width: 5.8rem;
padding: 0.45rem 0.55rem;
text-align: center;
border-right: 1px solid var(--table-border);
}
.dashboard-risk-item:last-child {
border-right: none;
}
.dashboard-risk-label {
font-size: 0.62rem;
line-height: 1.3;
color: var(--text-muted);
white-space: nowrap;
}
.dashboard-risk-value {
font-size: 0.8rem;
font-weight: 600;
color: var(--text-title);
margin-top: 0.18rem;
font-variant-numeric: tabular-nums;
white-space: nowrap;
} }
.dashboard-risk-grid .stat-item { .dashboard-risk-grid .stat-item {
@@ -84,13 +118,12 @@
.dash-symbol-title { .dash-symbol-title {
font-weight: 600; font-weight: 600;
line-height: 1.35; line-height: 1.45;
} }
.dash-symbol-sub { .dash-symbol-ex {
font-size: 0.72rem; font-weight: 400;
line-height: 1.35; font-size: 0.78rem;
margin-top: 0.12rem;
} }
.dash-main-badge { .dash-main-badge {
+54 -20
View File
@@ -59,30 +59,42 @@
.replace(/"/g, '&quot;'); .replace(/"/g, '&quot;');
} }
function slTpText(row) { function slText(row) {
var parts = []; if (row.trailing_be && row.stop_loss == null) return '移动保本';
if (row.stop_loss != null) parts.push('SL ' + fmtNum(row.stop_loss)); if (row.stop_loss != null) return fmtNum(row.stop_loss);
if (row.take_profit != null) parts.push('TP ' + fmtNum(row.take_profit)); return '—';
if (row.trailing_be) parts.push('移动保本'); }
return parts.length ? parts.join(' · ') : '—';
function tpText(row) {
if (row.trailing_be) return '移动保本';
if (row.take_profit != null) return fmtNum(row.take_profit);
return '—';
} }
function symbolCellHtml(row) { function symbolCellHtml(row) {
var name = row.symbol_name || row.symbol || ''; var name = row.symbol_name || row.symbol || '';
var code = row.symbol_code || ''; var code = row.symbol_code || '';
if (!code && row.symbol && String(row.symbol).toLowerCase() !== String(name).toLowerCase()) {
code = row.symbol;
}
var exchange = row.symbol_exchange || '';
var mainBadge = row.symbol_is_main var mainBadge = row.symbol_is_main
? ' <span class="badge planned dash-main-badge">主力</span>' : ''; ? ' <span class="badge planned dash-main-badge">主力</span>' : '';
var titleInner = escHtml(name) + mainBadge; var titleInner = escHtml(name);
if (exchange) {
titleInner += ' <span class="dash-symbol-ex text-muted">' + escHtml(exchange) + '</span>';
}
titleInner += mainBadge;
if (code && String(name).toLowerCase() !== String(code).toLowerCase()) { if (code && String(name).toLowerCase() !== String(code).toLowerCase()) {
titleInner += ' <span class="text-accent">' + escHtml(code) + '</span>'; titleInner += ' <span class="text-accent">' + escHtml(code) + '</span>';
} else if (!name && code) { } else if (!name && code) {
titleInner = '<span class="text-accent">' + escHtml(code) + '</span>'; titleInner = (exchange
? '<span class="dash-symbol-ex text-muted">' + escHtml(exchange) + '</span> '
: '') + '<span class="text-accent">' + escHtml(code) + '</span>';
} }
var sub = row.symbol_exchange || code || '';
return ( return (
'<div class="dash-symbol-cell">' + '<div class="dash-symbol-cell">' +
'<div class="dash-symbol-title">' + titleInner + '</div>' + '<div class="dash-symbol-title">' + titleInner + '</div>' +
(sub ? '<div class="dash-symbol-sub text-muted">' + escHtml(sub) + '</div>' : '') +
'</div>' '</div>'
); );
} }
@@ -132,6 +144,24 @@
var maxPos = lim.max_active_positions != null ? lim.max_active_positions : st.max_active_positions; var maxPos = lim.max_active_positions != null ? lim.max_active_positions : st.max_active_positions;
var manualCnt = risk.manual_close_count_today != null ? risk.manual_close_count_today : 0; var manualCnt = risk.manual_close_count_today != null ? risk.manual_close_count_today : 0;
var manualLim = lim.manual_close_daily_limit; var manualLim = lim.manual_close_daily_limit;
var dailyOpens = risk.daily_open_count != null
? risk.daily_open_count
: (st.daily_open_count != null ? st.daily_open_count : '—');
var dailyPosLim = lim.daily_position_limit != null
? lim.daily_position_limit
: st.daily_position_limit;
var dailyRiskUsed = risk.daily_risk_used_pct != null
? risk.daily_risk_used_pct
: st.daily_risk_used_pct;
var dailyRiskLim = lim.daily_trading_risk_pct_limit != null
? lim.daily_trading_risk_pct_limit
: st.daily_trading_risk_pct_limit;
var dailyRiskText = dailyRiskUsed != null ? fmtNum(dailyRiskUsed) + '%' : '—';
if (dailyRiskLim != null && dailyRiskUsed != null) {
dailyRiskText += ' / ' + fmtNum(dailyRiskLim) + '%';
} else if (dailyRiskLim != null) {
dailyRiskText += ' / ' + fmtNum(dailyRiskLim) + '%';
}
var marginPct = risk.margin_pct_used; var marginPct = risk.margin_pct_used;
var maxMarginPct = lim.max_margin_pct; var maxMarginPct = lim.max_margin_pct;
var marginPctText = marginPct != null ? fmtNum(marginPct) + '%' : '—'; var marginPctText = marginPct != null ? fmtNum(marginPct) + '%' : '—';
@@ -157,23 +187,24 @@
var items = [ var items = [
{ label: '风控开关', value: enabled ? '开启' : '关闭' }, { label: '风控开关', value: enabled ? '开启' : '关闭' },
{ label: '持仓限制', value: active + ' / ' + (maxPos != null ? maxPos : '—') }, { label: '持仓限制', value: active + ' / ' + (maxPos != null ? maxPos : '—') },
{ label: '日手动平仓', value: manualCnt + ' / ' + (manualLim != null ? manualLim : '—') }, { label: '日持仓限制', value: dailyOpens + ' / ' + (dailyPosLim != null ? dailyPosLim : '—') },
{ label: '日交易风险', value: dailyRiskText },
{ label: '手动平仓(冷静期触发)', value: manualCnt + ' / ' + (manualLim != null ? manualLim : '—') },
{ label: '冷静期(默认)', value: fmtHours(lim.cooling_hours_manual) }, { label: '冷静期(默认)', value: fmtHours(lim.cooling_hours_manual) },
{ label: '复盘后冷静', value: fmtHours(lim.cooling_hours_manual_journal) }, { label: '复盘后冷静', value: fmtHours(lim.cooling_hours_manual_journal) },
{ label: '冷静剩余', value: fmtRemainSec(st.freeze_remaining_sec) }, { label: '冷静剩余', value: fmtRemainSec(st.freeze_remaining_sec) },
{ label: '保证金占比', value: marginPctText }, { label: '保证金占比', value: marginPctText },
{ label: '保证金上限', value: maxMarginPct != null ? fmtNum(maxMarginPct) + '%' : '—' }, { label: '保证金上限', value: maxMarginPct != null ? fmtNum(maxMarginPct) + '%' : '—' },
{ label: '滚仓保证金上限', value: lim.roll_max_margin_pct != null ? fmtNum(lim.roll_max_margin_pct) + '%' : '—' }, { label: '综合保证金上限', value: lim.roll_max_margin_pct != null ? fmtNum(lim.roll_max_margin_pct) + '%' : '—' },
{ label: '计仓模式', value: sizingDetail }, { label: '计仓模式', value: sizingDetail },
{ label: '单笔风险', value: lim.risk_percent != null ? fmtNum(lim.risk_percent) + '%' : '—' },
{ label: '交易日切', value: lim.trading_day_reset_hour != null ? lim.trading_day_reset_hour + ':00' : '—' } { label: '交易日切', value: lim.trading_day_reset_hour != null ? lim.trading_day_reset_hour + ':00' : '—' }
]; ];
riskGridEl.innerHTML = items.map(function (it) { riskGridEl.innerHTML = items.map(function (it) {
return ( return (
'<div class="stat-item">' + '<div class="dashboard-risk-item">' +
'<div class="label">' + escHtml(it.label) + '</div>' + '<div class="dashboard-risk-label">' + escHtml(it.label) + '</div>' +
'<div class="value">' + escHtml(it.value) + '</div>' + '<div class="dashboard-risk-value">' + escHtml(it.value) + '</div>' +
'</div>' '</div>'
); );
}).join(''); }).join('');
@@ -320,7 +351,7 @@
function renderPositions(rows) { function renderPositions(rows) {
if (!posBody) return; if (!posBody) return;
if (!rows.length) { if (!rows.length) {
posBody.innerHTML = '<tr><td colspan="8" class="text-muted">暂无持仓</td></tr>'; posBody.innerHTML = '<tr><td colspan="9" class="text-muted">暂无持仓</td></tr>';
posRowCache = {}; posRowCache = {};
return; return;
} }
@@ -337,7 +368,8 @@
'<td class="dash-p-mark">' + (r.current_price != null ? fmtNum(r.current_price) : '—') + '</td>' + '<td class="dash-p-mark">' + (r.current_price != null ? fmtNum(r.current_price) : '—') + '</td>' +
'<td class="dash-p-pnl ' + pnlClass(r.float_pnl) + '">' + fmtPnl(r.float_pnl) + '</td>' + '<td class="dash-p-pnl ' + pnlClass(r.float_pnl) + '">' + fmtPnl(r.float_pnl) + '</td>' +
'<td class="dash-p-margin">' + (r.margin != null ? fmtMoney(r.margin) : '—') + '</td>' + '<td class="dash-p-margin">' + (r.margin != null ? fmtMoney(r.margin) : '—') + '</td>' +
'<td class="dash-p-sltp">' + escHtml(slTpText(r)) + '</td>' + '<td class="dash-p-sl">' + escHtml(slText(r)) + '</td>' +
'<td class="dash-p-tp">' + escHtml(tpText(r)) + '</td>' +
'</tr>' '</tr>'
); );
}).join(''); }).join('');
@@ -396,7 +428,8 @@
var markEl = row.querySelector('.dash-p-mark'); var markEl = row.querySelector('.dash-p-mark');
var pnlEl = row.querySelector('.dash-p-pnl'); var pnlEl = row.querySelector('.dash-p-pnl');
var marginEl = row.querySelector('.dash-p-margin'); var marginEl = row.querySelector('.dash-p-margin');
var sltpEl = row.querySelector('.dash-p-sltp'); var slEl = row.querySelector('.dash-p-sl');
var tpEl = row.querySelector('.dash-p-tp');
if (lotsEl) lotsEl.textContent = String(r.lots); if (lotsEl) lotsEl.textContent = String(r.lots);
if (entryEl) entryEl.textContent = fmtNum(r.entry_price); if (entryEl) entryEl.textContent = fmtNum(r.entry_price);
if (markEl) markEl.textContent = r.current_price != null ? fmtNum(r.current_price) : '—'; if (markEl) markEl.textContent = r.current_price != null ? fmtNum(r.current_price) : '—';
@@ -405,7 +438,8 @@
pnlEl.className = 'dash-p-pnl ' + pnlClass(r.float_pnl); pnlEl.className = 'dash-p-pnl ' + pnlClass(r.float_pnl);
} }
if (marginEl) marginEl.textContent = r.margin != null ? fmtMoney(r.margin) : '—'; if (marginEl) marginEl.textContent = r.margin != null ? fmtMoney(r.margin) : '—';
if (sltpEl) sltpEl.textContent = slTpText(r); if (slEl) slEl.textContent = slText(r);
if (tpEl) tpEl.textContent = tpText(r);
}); });
} }
} }
+3 -2
View File
@@ -51,11 +51,12 @@
<th>现价</th> <th>现价</th>
<th>浮盈亏</th> <th>浮盈亏</th>
<th>保证金</th> <th>保证金</th>
<th>止损/止盈</th> <th>止损</th>
<th>止盈</th>
</tr> </tr>
</thead> </thead>
<tbody id="dash-positions-body"> <tbody id="dash-positions-body">
<tr><td colspan="8" class="text-muted">加载中…</td></tr> <tr><td colspan="9" class="text-muted">加载中…</td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>