对齐币本位期权:现货缓冲开仓、页头 ETH/BTC 余额与默认 coin 模式。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,194 +1,249 @@
|
||||
"""embed 壳/片段:按 tab 裁剪 render_main_page 的数据加载,降内存与 API 压力."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
EMBED_STRATEGY_PAGES = frozenset()
|
||||
|
||||
_WIN_EPS = 1e-9
|
||||
|
||||
|
||||
def env_truthy(raw: str | None, default: bool = False) -> bool:
|
||||
if raw is None or str(raw).strip() == "":
|
||||
return default
|
||||
return str(raw).strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
def show_perp_funds_enabled(*, exchange_key: str | None = None) -> bool:
|
||||
"""OKX:是否在顶栏显示永续资金账户/交易账户.其他所恒为 True."""
|
||||
ex = (exchange_key or "").strip().lower()
|
||||
if ex and ex != "okx":
|
||||
return True
|
||||
return env_truthy(os.getenv("OKX_SHOW_PERP_FUNDS"), default=True)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EmbedRenderPlan:
|
||||
exchange_capitals: bool
|
||||
records_rows: bool
|
||||
records_summary: bool
|
||||
key_history: bool
|
||||
key_list: bool
|
||||
orders: bool
|
||||
stats_bundle: bool
|
||||
strategy: bool
|
||||
orphan_live: bool
|
||||
|
||||
|
||||
def embed_render_plan(page: str, embed_mode: str | None) -> EmbedRenderPlan:
|
||||
if embed_mode not in ("fragment", "shell"):
|
||||
return EmbedRenderPlan(
|
||||
exchange_capitals=True,
|
||||
records_rows=True,
|
||||
records_summary=False,
|
||||
key_history=True,
|
||||
key_list=True,
|
||||
orders=True,
|
||||
stats_bundle=True,
|
||||
strategy=True,
|
||||
orphan_live=True,
|
||||
)
|
||||
is_shell = embed_mode == "shell"
|
||||
is_strategy = page in EMBED_STRATEGY_PAGES
|
||||
return EmbedRenderPlan(
|
||||
exchange_capitals=is_shell,
|
||||
records_rows=False, # 永续交易记录页已移除
|
||||
# 顶栏常驻:设置/风控/env 也要统计,否则首屏 SSR 为 0 后软切 tab 不会重绘顶栏
|
||||
records_summary=False,
|
||||
key_history=page == "key_monitor",
|
||||
key_list=page == "key_monitor" or is_strategy,
|
||||
orders=False, # 实盘下单界面已移除;对冲永续下单不依赖本页数据
|
||||
stats_bundle=False,
|
||||
strategy=is_strategy,
|
||||
orphan_live=False,
|
||||
)
|
||||
|
||||
|
||||
def profit_loss_ratio_from_averages(avg_win: float | None, avg_loss: float | None) -> float | None:
|
||||
"""盈亏比 = 平均盈利 / |平均亏损|."""
|
||||
if avg_win is None or avg_loss is None:
|
||||
return None
|
||||
try:
|
||||
aw = float(avg_win)
|
||||
al = float(avg_loss)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if al == 0:
|
||||
return None
|
||||
return round(aw / abs(al), 2)
|
||||
|
||||
|
||||
def profit_loss_ratio_from_trades(trades: list[dict[str, Any]] | None) -> float | None:
|
||||
wins: list[float] = []
|
||||
losses: list[float] = []
|
||||
for row in trades or []:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
try:
|
||||
pnl = float(row.get("effective_pnl_amount") or row.get("pnl_amount") or 0)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if pnl > _WIN_EPS:
|
||||
wins.append(pnl)
|
||||
elif pnl < -_WIN_EPS:
|
||||
losses.append(pnl)
|
||||
avg_win = sum(wins) / len(wins) if wins else None
|
||||
avg_loss = sum(losses) / len(losses) if losses else None
|
||||
return profit_loss_ratio_from_averages(avg_win, avg_loss)
|
||||
|
||||
|
||||
def options_funding_label(
|
||||
funding_usdc: float | None,
|
||||
funding_usdt: float | None = None,
|
||||
) -> str:
|
||||
"""期权侧顶栏仅展示 USDC(USDT 归永续资金/交易账户).funding_usdt 参数保留兼容,忽略."""
|
||||
_ = funding_usdt
|
||||
if funding_usdc is None:
|
||||
return "—"
|
||||
try:
|
||||
return f"{float(funding_usdc):.2f} USDC"
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
|
||||
|
||||
def total_funds_usdt(
|
||||
funding_usdt: float | None,
|
||||
trading_usdt: float | None,
|
||||
options_trading_usdc: float | None = None,
|
||||
options_funding_usdc: float | None = None,
|
||||
options_funding_usdt: float | None = None,
|
||||
options_trading_usdt: float | None = None,
|
||||
) -> float | None:
|
||||
parts = [
|
||||
funding_usdt,
|
||||
trading_usdt,
|
||||
options_funding_usdc,
|
||||
options_funding_usdt,
|
||||
options_trading_usdc,
|
||||
options_trading_usdt,
|
||||
]
|
||||
if all(v is None for v in parts):
|
||||
return None
|
||||
try:
|
||||
total = 0.0
|
||||
for v in parts:
|
||||
if v is not None:
|
||||
total += float(v)
|
||||
return round(total, 2)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def trade_records_summary(conn, start_bj: str, end_bj: str, tr_ts: str) -> dict[str, Any]:
|
||||
"""顶栏统计用 COUNT,避免 embed 壳拉 1000 行交易记录."""
|
||||
from lib.trade.trade_result_lib import sql_effective_pnl_expr
|
||||
|
||||
pnl_sql = sql_effective_pnl_expr()
|
||||
row = conn.execute(
|
||||
f"""
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
SUM(CASE WHEN {pnl_sql} > 0 THEN 1 ELSE 0 END) AS wins,
|
||||
AVG(CASE WHEN {pnl_sql} > 0 THEN {pnl_sql} END) AS avg_win,
|
||||
AVG(CASE WHEN {pnl_sql} < 0 THEN {pnl_sql} END) AS avg_loss
|
||||
FROM trade_records
|
||||
WHERE {tr_ts} >= ? AND {tr_ts} <= ?
|
||||
AND COALESCE(result, '') != '错过'
|
||||
AND COALESCE(reviewed_result, '') != '错过'
|
||||
""",
|
||||
(start_bj, end_bj),
|
||||
).fetchone()
|
||||
total = int(row["total"] or 0) if row else 0
|
||||
wins = int(row["wins"] or 0) if row else 0
|
||||
rate = round(wins / total * 100, 2) if total else 0
|
||||
avg_win = float(row["avg_win"]) if row and row["avg_win"] is not None else None
|
||||
avg_loss = float(row["avg_loss"]) if row and row["avg_loss"] is not None else None
|
||||
return {
|
||||
"records": [],
|
||||
"total": total,
|
||||
"rate": rate,
|
||||
"profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss),
|
||||
}
|
||||
|
||||
|
||||
def header_trade_stats_for_window(conn, list_window: dict[str, Any], app_tz) -> dict[str, Any]:
|
||||
"""account_snapshot / 顶栏刷新:按当前列表窗返回总交易/胜率/盈亏比."""
|
||||
from lib.common.history_window_lib import sql_list_time_field, utc_window_to_bj_sql_strings
|
||||
|
||||
start_bj, end_bj = utc_window_to_bj_sql_strings(
|
||||
list_window["start_utc"], list_window["end_utc"], app_tz
|
||||
)
|
||||
tr_ts = sql_list_time_field("closed_at", "created_at", "opened_at")
|
||||
summary = trade_records_summary(conn, start_bj, end_bj, tr_ts)
|
||||
return {
|
||||
"total": summary["total"],
|
||||
"rate": summary["rate"],
|
||||
"profit_loss_ratio": summary.get("profit_loss_ratio"),
|
||||
}
|
||||
|
||||
|
||||
def minimal_stats_bundle(reset_hour: int) -> dict[str, Any]:
|
||||
return {"stats_reset_hour": reset_hour, "segments": []}
|
||||
"""embed 壳/片段:按 tab 裁剪 render_main_page 的数据加载,降内存与 API 压力."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
EMBED_STRATEGY_PAGES = frozenset({"strategy", "strategy_trend", "strategy_roll", "strategy_records"})
|
||||
|
||||
_WIN_EPS = 1e-9
|
||||
|
||||
|
||||
def env_truthy(raw: str | None, default: bool = False) -> bool:
|
||||
if raw is None or str(raw).strip() == "":
|
||||
return default
|
||||
return str(raw).strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
def show_perp_funds_enabled(*, exchange_key: str | None = None) -> bool:
|
||||
"""OKX:是否在顶栏显示永续资金账户/交易账户.其他所恒为 True."""
|
||||
ex = (exchange_key or "").strip().lower()
|
||||
if ex and ex != "okx":
|
||||
return True
|
||||
return env_truthy(os.getenv("OKX_SHOW_PERP_FUNDS"), default=True)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EmbedRenderPlan:
|
||||
exchange_capitals: bool
|
||||
records_rows: bool
|
||||
records_summary: bool
|
||||
key_history: bool
|
||||
key_list: bool
|
||||
orders: bool
|
||||
stats_bundle: bool
|
||||
strategy: bool
|
||||
orphan_live: bool
|
||||
|
||||
|
||||
def embed_render_plan(page: str, embed_mode: str | None) -> EmbedRenderPlan:
|
||||
if embed_mode not in ("fragment", "shell"):
|
||||
return EmbedRenderPlan(
|
||||
exchange_capitals=True,
|
||||
records_rows=True,
|
||||
records_summary=False,
|
||||
key_history=True,
|
||||
key_list=True,
|
||||
orders=True,
|
||||
stats_bundle=True,
|
||||
strategy=True,
|
||||
orphan_live=True,
|
||||
)
|
||||
is_shell = embed_mode == "shell"
|
||||
is_strategy = page in EMBED_STRATEGY_PAGES
|
||||
return EmbedRenderPlan(
|
||||
exchange_capitals=is_shell,
|
||||
records_rows=page == "records",
|
||||
# 顶栏常驻:设置/风控/env 也要统计,否则首屏 SSR 为 0 后软切 tab 不会重绘顶栏
|
||||
records_summary=is_shell and page != "records",
|
||||
key_history=page == "key_monitor",
|
||||
key_list=page in ("key_monitor", "trade") or is_strategy,
|
||||
orders=page == "trade" or is_strategy,
|
||||
stats_bundle=page == "stats",
|
||||
strategy=is_strategy,
|
||||
orphan_live=page == "trade" and is_shell,
|
||||
)
|
||||
|
||||
|
||||
def profit_loss_ratio_from_averages(avg_win: float | None, avg_loss: float | None) -> float | None:
|
||||
"""盈亏比 = 平均盈利 / |平均亏损|."""
|
||||
if avg_win is None or avg_loss is None:
|
||||
return None
|
||||
try:
|
||||
aw = float(avg_win)
|
||||
al = float(avg_loss)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if al == 0:
|
||||
return None
|
||||
return round(aw / abs(al), 2)
|
||||
|
||||
|
||||
def profit_loss_ratio_from_trades(trades: list[dict[str, Any]] | None) -> float | None:
|
||||
wins: list[float] = []
|
||||
losses: list[float] = []
|
||||
for row in trades or []:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
try:
|
||||
pnl = float(row.get("effective_pnl_amount") or row.get("pnl_amount") or 0)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if pnl > _WIN_EPS:
|
||||
wins.append(pnl)
|
||||
elif pnl < -_WIN_EPS:
|
||||
losses.append(pnl)
|
||||
avg_win = sum(wins) / len(wins) if wins else None
|
||||
avg_loss = sum(losses) / len(losses) if losses else None
|
||||
return profit_loss_ratio_from_averages(avg_win, avg_loss)
|
||||
|
||||
|
||||
def options_funding_label(
|
||||
funding_usdc: float | None,
|
||||
funding_usdt: float | None = None,
|
||||
funding_eth: float | None = None,
|
||||
margin_mode: str | None = None,
|
||||
underly: str = "ETH",
|
||||
) -> str:
|
||||
"""期权侧顶栏文案(仅 USDC 模式使用;币本位不展示期权资金/交易两列)."""
|
||||
if funding_usdc is None:
|
||||
return "—"
|
||||
try:
|
||||
return f"{float(funding_usdc):.2f} USDC"
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
|
||||
|
||||
def _fmt_coin_amount(v: float | None, *, min_amt: float = 1e-6) -> str | None:
|
||||
if v is None:
|
||||
return None
|
||||
try:
|
||||
n = float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if n < min_amt:
|
||||
return None
|
||||
txt = f"{n:.6f}".rstrip("0").rstrip(".")
|
||||
return txt or None
|
||||
|
||||
|
||||
def trading_account_label(
|
||||
usdt: float | None,
|
||||
eth: float | None = None,
|
||||
btc: float | None = None,
|
||||
*,
|
||||
margin_mode: str | None = None,
|
||||
) -> str:
|
||||
"""交易账户顶栏文案.
|
||||
|
||||
币本位:USDT / ETH / BTC(有余额才带上,不显示其它币种).
|
||||
其它模式:xx.xxU.
|
||||
"""
|
||||
try:
|
||||
from lib.options.options_margin_mode_lib import normalize_options_margin_mode
|
||||
|
||||
mode = normalize_options_margin_mode(margin_mode)
|
||||
except Exception:
|
||||
mode = str(margin_mode or "coin").strip().lower() or "coin"
|
||||
if mode != "coin":
|
||||
if usdt is None:
|
||||
return "—"
|
||||
try:
|
||||
return f"{float(usdt):.2f}U"
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
parts: list[str] = []
|
||||
if usdt is not None:
|
||||
try:
|
||||
parts.append(f"{float(usdt):.2f} USDT")
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
eth_txt = _fmt_coin_amount(eth, min_amt=1e-6)
|
||||
if eth_txt is not None:
|
||||
parts.append(f"{eth_txt} ETH")
|
||||
btc_txt = _fmt_coin_amount(btc, min_amt=1e-7)
|
||||
if btc_txt is not None:
|
||||
parts.append(f"{btc_txt} BTC")
|
||||
return " / ".join(parts) if parts else "—"
|
||||
|
||||
|
||||
def total_funds_usdt(
|
||||
funding_usdt: float | None,
|
||||
trading_usdt: float | None,
|
||||
options_trading_usdc: float | None = None,
|
||||
options_funding_usdc: float | None = None,
|
||||
options_funding_usdt: float | None = None,
|
||||
options_trading_usdt: float | None = None,
|
||||
) -> float | None:
|
||||
parts = [
|
||||
funding_usdt,
|
||||
trading_usdt,
|
||||
options_funding_usdc,
|
||||
options_funding_usdt,
|
||||
options_trading_usdc,
|
||||
options_trading_usdt,
|
||||
]
|
||||
if all(v is None for v in parts):
|
||||
return None
|
||||
try:
|
||||
total = 0.0
|
||||
for v in parts:
|
||||
if v is not None:
|
||||
total += float(v)
|
||||
return round(total, 2)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def trade_records_summary(conn, start_bj: str, end_bj: str, tr_ts: str) -> dict[str, Any]:
|
||||
"""顶栏统计用 COUNT,避免 embed 壳拉 1000 行交易记录."""
|
||||
from lib.trade.trade_result_lib import sql_effective_pnl_expr
|
||||
|
||||
pnl_sql = sql_effective_pnl_expr()
|
||||
row = conn.execute(
|
||||
f"""
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
SUM(CASE WHEN {pnl_sql} > 0 THEN 1 ELSE 0 END) AS wins,
|
||||
AVG(CASE WHEN {pnl_sql} > 0 THEN {pnl_sql} END) AS avg_win,
|
||||
AVG(CASE WHEN {pnl_sql} < 0 THEN {pnl_sql} END) AS avg_loss
|
||||
FROM trade_records
|
||||
WHERE {tr_ts} >= ? AND {tr_ts} <= ?
|
||||
AND COALESCE(result, '') != '错过'
|
||||
AND COALESCE(reviewed_result, '') != '错过'
|
||||
""",
|
||||
(start_bj, end_bj),
|
||||
).fetchone()
|
||||
total = int(row["total"] or 0) if row else 0
|
||||
wins = int(row["wins"] or 0) if row else 0
|
||||
rate = round(wins / total * 100, 2) if total else 0
|
||||
avg_win = float(row["avg_win"]) if row and row["avg_win"] is not None else None
|
||||
avg_loss = float(row["avg_loss"]) if row and row["avg_loss"] is not None else None
|
||||
return {
|
||||
"records": [],
|
||||
"total": total,
|
||||
"rate": rate,
|
||||
"profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss),
|
||||
}
|
||||
|
||||
|
||||
def header_trade_stats_for_window(conn, list_window: dict[str, Any], app_tz) -> dict[str, Any]:
|
||||
"""account_snapshot / 顶栏刷新:按当前列表窗返回总交易/胜率/盈亏比."""
|
||||
from lib.common.history_window_lib import sql_list_time_field, utc_window_to_bj_sql_strings
|
||||
|
||||
start_bj, end_bj = utc_window_to_bj_sql_strings(
|
||||
list_window["start_utc"], list_window["end_utc"], app_tz
|
||||
)
|
||||
tr_ts = sql_list_time_field("closed_at", "created_at", "opened_at")
|
||||
summary = trade_records_summary(conn, start_bj, end_bj, tr_ts)
|
||||
return {
|
||||
"total": summary["total"],
|
||||
"rate": summary["rate"],
|
||||
"profit_loss_ratio": summary.get("profit_loss_ratio"),
|
||||
}
|
||||
|
||||
|
||||
def minimal_stats_bundle(reset_hour: int) -> dict[str, Any]:
|
||||
return {"stats_reset_hour": reset_hour, "segments": []}
|
||||
|
||||
@@ -1,170 +1,194 @@
|
||||
"""实例系统设置 API:导航开关,env 读写,改密,PM2 重启."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from functools import wraps
|
||||
from typing import Any, Callable
|
||||
|
||||
from flask import jsonify, request, session
|
||||
|
||||
from lib.env.env_file_lib import apply_env_updates, env_get, read_env_lines
|
||||
from lib.env.env_ui_manifest import (
|
||||
build_env_ui_payload,
|
||||
filter_updates_for_ui,
|
||||
coerce_hedge_partial_close_with_manual,
|
||||
validate_env_ui_updates,
|
||||
)
|
||||
from lib.env.env_schema import parse_env_example_schema
|
||||
from lib.instance.instance_display_prefs_lib import (
|
||||
display_meta_for_ui,
|
||||
get_display_prefs,
|
||||
normalize_display_prefs,
|
||||
save_display_prefs,
|
||||
tab_allowed,
|
||||
)
|
||||
from lib.instance.instance_pm2_lib import restart_instance_pm2
|
||||
from lib.instance.runtime_config_lib import apply_env_reload
|
||||
|
||||
|
||||
def _api_login_required():
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
logged_in = bool(session.get("logged_in"))
|
||||
auth_disabled = (os.getenv("APP_AUTH_DISABLED") or "").strip().lower() in (
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
"on",
|
||||
)
|
||||
if auth_disabled or logged_in:
|
||||
return f(*args, **kwargs)
|
||||
return jsonify({"ok": False, "msg": "未登录"}), 401
|
||||
|
||||
return wrapped
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_instance_settings_routes(
|
||||
app,
|
||||
*,
|
||||
get_db: Callable,
|
||||
login_required_fn: Callable,
|
||||
base_dir: str,
|
||||
exchange_key: str,
|
||||
username: str,
|
||||
password: str,
|
||||
) -> None:
|
||||
env_path = os.path.join(base_dir, ".env")
|
||||
example_path = os.path.join(base_dir, ".env.example")
|
||||
api_auth = _api_login_required()
|
||||
|
||||
@app.route("/api/settings/display", methods=["GET", "POST"])
|
||||
@api_auth
|
||||
def api_settings_display():
|
||||
if request.method == "GET":
|
||||
prefs = get_display_prefs(get_db)
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"display": prefs,
|
||||
"meta": display_meta_for_ui(),
|
||||
}
|
||||
)
|
||||
body = request.get_json(silent=True) or {}
|
||||
raw = body.get("display") if isinstance(body.get("display"), dict) else body
|
||||
saved = save_display_prefs(get_db, raw)
|
||||
return jsonify({"ok": True, "display": saved})
|
||||
|
||||
@app.route("/api/settings/env/meta", methods=["GET"])
|
||||
@api_auth
|
||||
def api_env_meta():
|
||||
groups = build_env_ui_payload(exchange_key, example_path, env_path)
|
||||
return jsonify({"ok": True, "groups": groups})
|
||||
|
||||
@app.route("/api/settings/env", methods=["GET", "POST"])
|
||||
@api_auth
|
||||
def api_settings_env():
|
||||
if request.method == "GET":
|
||||
groups = build_env_ui_payload(exchange_key, example_path, env_path)
|
||||
return jsonify({"ok": True, "groups": groups})
|
||||
body = request.get_json(silent=True) or {}
|
||||
updates = body.get("values") if isinstance(body.get("values"), dict) else body
|
||||
if not isinstance(updates, dict):
|
||||
return jsonify({"ok": False, "msg": "无效请求体"}), 400
|
||||
updates = filter_updates_for_ui(exchange_key, updates)
|
||||
clean, errors = validate_env_ui_updates(exchange_key, example_path, updates)
|
||||
if errors:
|
||||
return jsonify({"ok": False, "msg": "; ".join(errors)}), 400
|
||||
clean = coerce_hedge_partial_close_with_manual(clean, env_path=env_path)
|
||||
if not clean:
|
||||
return jsonify({"ok": True, "changed_keys": [], "restart_required": False})
|
||||
changed = apply_env_updates(env_path, clean)
|
||||
groups = parse_env_example_schema(example_path)
|
||||
reload_info = apply_env_reload(env_path, get_db, changed, groups)
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"changed_keys": changed,
|
||||
"restart_required": reload_info.get("restart_required", False),
|
||||
}
|
||||
)
|
||||
|
||||
@app.route("/api/settings/password", methods=["POST"])
|
||||
@api_auth
|
||||
def api_change_password():
|
||||
body = request.get_json(silent=True) or {}
|
||||
old_password = str(body.get("old_password") or "")
|
||||
new_username = str(body.get("new_username") or "").strip()
|
||||
new_password = str(body.get("new_password") or "")
|
||||
confirm = str(body.get("confirm_password") or "")
|
||||
if not old_password or old_password != password:
|
||||
return jsonify({"ok": False, "msg": "当前密码错误"}), 400
|
||||
if len(new_password) < 6:
|
||||
return jsonify({"ok": False, "msg": "新密码至少 6 位"}), 400
|
||||
if new_password != confirm:
|
||||
return jsonify({"ok": False, "msg": "两次输入的新密码不一致"}), 400
|
||||
updates: dict[str, str] = {"APP_PASSWORD": new_password}
|
||||
if new_username:
|
||||
updates["APP_USERNAME"] = new_username
|
||||
changed = apply_env_updates(env_path, updates)
|
||||
groups = parse_env_example_schema(example_path)
|
||||
apply_env_reload(env_path, get_db, changed, groups)
|
||||
return jsonify({"ok": True, "restart_required": True, "changed_keys": changed})
|
||||
|
||||
@app.route("/api/admin/restart", methods=["POST"])
|
||||
@api_auth
|
||||
def api_admin_restart():
|
||||
result = restart_instance_pm2(exchange_key, defer=True)
|
||||
code = 200 if result.get("ok") else 500
|
||||
return jsonify({"ok": bool(result.get("ok")), **result}), code
|
||||
|
||||
@app.route("/api/admin/health", methods=["GET"])
|
||||
def api_admin_health():
|
||||
return jsonify({"ok": True, "status": "up"})
|
||||
|
||||
def tab_allowed_fn(tab: str) -> bool:
|
||||
prefs = get_display_prefs(get_db)
|
||||
return tab_allowed(tab, prefs)
|
||||
|
||||
app.config["INSTANCE_GET_DB"] = get_db
|
||||
app.config["INSTANCE_TAB_ALLOWED_FN"] = tab_allowed_fn
|
||||
|
||||
@app.route("/api/embed/tab_allowed/<tab>", methods=["GET"])
|
||||
@api_auth
|
||||
def api_tab_allowed(tab: str):
|
||||
prefs = get_display_prefs(get_db)
|
||||
return jsonify({"ok": True, "tab": tab, "allowed": tab_allowed(tab, prefs)})
|
||||
|
||||
|
||||
def merge_ui_template_context(page: str, get_db: Callable, **settings_kwargs: Any) -> dict[str, Any]:
|
||||
from lib.instance.instance_settings_lib import settings_page_context
|
||||
|
||||
prefs = get_display_prefs(get_db)
|
||||
ctx = {
|
||||
"display": prefs,
|
||||
"display_meta": display_meta_for_ui(),
|
||||
**settings_page_context(page, display=prefs, **settings_kwargs),
|
||||
}
|
||||
return ctx
|
||||
"""实例系统设置 API:导航开关,env 读写,改密,PM2 重启."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from functools import wraps
|
||||
from typing import Any, Callable
|
||||
|
||||
from flask import jsonify, request, session
|
||||
|
||||
from lib.env.env_file_lib import apply_env_updates, env_get, read_env_lines
|
||||
from lib.env.env_ui_manifest import (
|
||||
build_env_ui_payload,
|
||||
filter_updates_for_ui,
|
||||
coerce_hedge_partial_close_with_manual,
|
||||
validate_env_ui_updates,
|
||||
)
|
||||
from lib.env.env_schema import parse_env_example_schema
|
||||
from lib.instance.instance_display_prefs_lib import (
|
||||
display_meta_for_ui,
|
||||
get_display_prefs,
|
||||
normalize_display_prefs,
|
||||
save_display_prefs,
|
||||
tab_allowed,
|
||||
)
|
||||
from lib.instance.instance_pm2_lib import restart_instance_pm2
|
||||
from lib.instance.runtime_config_lib import apply_env_reload
|
||||
|
||||
|
||||
def _api_login_required(hub_token_write_allowed: bool = False):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
from lib.hub.hub_auth import request_allowed as hub_request_allowed
|
||||
|
||||
logged_in = bool(session.get("logged_in"))
|
||||
auth_disabled = (os.getenv("APP_AUTH_DISABLED") or "").strip().lower() in (
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
"on",
|
||||
)
|
||||
hub_hdr = (request.headers.get("X-Hub-Token") or "").strip()
|
||||
bridge = (os.getenv("HUB_BRIDGE_TOKEN") or "").strip()
|
||||
if hub_hdr and bridge and hub_hdr == bridge and not hub_token_write_allowed:
|
||||
return jsonify({"ok": False, "msg": "Hub Token 不可修改实例设置"}), 403
|
||||
if hub_request_allowed(logged_in, auth_disabled):
|
||||
return f(*args, **kwargs)
|
||||
return jsonify({"ok": False, "msg": "未登录"}), 401
|
||||
|
||||
return wrapped
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_instance_settings_routes(
|
||||
app,
|
||||
*,
|
||||
get_db: Callable,
|
||||
login_required_fn: Callable,
|
||||
base_dir: str,
|
||||
exchange_key: str,
|
||||
username: str,
|
||||
password: str,
|
||||
) -> None:
|
||||
env_path = os.path.join(base_dir, ".env")
|
||||
example_path = os.path.join(base_dir, ".env.example")
|
||||
api_auth = _api_login_required()
|
||||
|
||||
@app.route("/api/settings/display", methods=["GET", "POST"])
|
||||
@api_auth
|
||||
def api_settings_display():
|
||||
if request.method == "GET":
|
||||
prefs = get_display_prefs(get_db)
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"display": prefs,
|
||||
"meta": display_meta_for_ui(),
|
||||
}
|
||||
)
|
||||
body = request.get_json(silent=True) or {}
|
||||
raw = body.get("display") if isinstance(body.get("display"), dict) else body
|
||||
saved = save_display_prefs(get_db, raw)
|
||||
return jsonify({"ok": True, "display": saved})
|
||||
|
||||
@app.route("/api/settings/env/meta", methods=["GET"])
|
||||
@api_auth
|
||||
def api_env_meta():
|
||||
groups = build_env_ui_payload(exchange_key, example_path, env_path)
|
||||
return jsonify({"ok": True, "groups": groups})
|
||||
|
||||
@app.route("/api/settings/env", methods=["GET", "POST"])
|
||||
@api_auth
|
||||
def api_settings_env():
|
||||
if request.method == "GET":
|
||||
groups = build_env_ui_payload(exchange_key, example_path, env_path)
|
||||
return jsonify({"ok": True, "groups": groups})
|
||||
body = request.get_json(silent=True) or {}
|
||||
updates = body.get("values") if isinstance(body.get("values"), dict) else body
|
||||
if not isinstance(updates, dict):
|
||||
return jsonify({"ok": False, "msg": "无效请求体"}), 400
|
||||
updates = filter_updates_for_ui(exchange_key, updates)
|
||||
clean, errors = validate_env_ui_updates(exchange_key, example_path, updates)
|
||||
if errors:
|
||||
return jsonify({"ok": False, "msg": "; ".join(errors)}), 400
|
||||
clean = coerce_hedge_partial_close_with_manual(clean, env_path=env_path)
|
||||
if not clean:
|
||||
return jsonify({"ok": True, "changed_keys": [], "restart_required": False})
|
||||
if "OKX_OPTIONS_MARGIN_MODE" in clean:
|
||||
try:
|
||||
from lib.options.options_margin_mode_lib import normalize_options_margin_mode
|
||||
from lib.options.options_spot_bridge_lib import mode_switch_block_msg
|
||||
|
||||
lines = read_env_lines(env_path)
|
||||
old_mode = normalize_options_margin_mode(env_get(lines, "OKX_OPTIONS_MARGIN_MODE") or "coin")
|
||||
new_mode = normalize_options_margin_mode(clean.get("OKX_OPTIONS_MARGIN_MODE"))
|
||||
if old_mode != new_mode:
|
||||
conn_m = get_db()
|
||||
try:
|
||||
block = mode_switch_block_msg(conn_m, None)
|
||||
if block:
|
||||
return jsonify({"ok": False, "msg": block}), 400
|
||||
finally:
|
||||
conn_m.close()
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": f"本位切换校验失败: {e}"}), 400
|
||||
changed = apply_env_updates(env_path, clean)
|
||||
groups = parse_env_example_schema(example_path)
|
||||
reload_info = apply_env_reload(env_path, get_db, changed, groups)
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"changed_keys": changed,
|
||||
"restart_required": reload_info.get("restart_required", False),
|
||||
}
|
||||
)
|
||||
|
||||
@app.route("/api/settings/password", methods=["POST"])
|
||||
@api_auth
|
||||
def api_change_password():
|
||||
body = request.get_json(silent=True) or {}
|
||||
old_password = str(body.get("old_password") or "")
|
||||
new_username = str(body.get("new_username") or "").strip()
|
||||
new_password = str(body.get("new_password") or "")
|
||||
confirm = str(body.get("confirm_password") or "")
|
||||
if not old_password or old_password != password:
|
||||
return jsonify({"ok": False, "msg": "当前密码错误"}), 400
|
||||
if len(new_password) < 6:
|
||||
return jsonify({"ok": False, "msg": "新密码至少 6 位"}), 400
|
||||
if new_password != confirm:
|
||||
return jsonify({"ok": False, "msg": "两次输入的新密码不一致"}), 400
|
||||
updates: dict[str, str] = {"APP_PASSWORD": new_password}
|
||||
if new_username:
|
||||
updates["APP_USERNAME"] = new_username
|
||||
changed = apply_env_updates(env_path, updates)
|
||||
groups = parse_env_example_schema(example_path)
|
||||
apply_env_reload(env_path, get_db, changed, groups)
|
||||
return jsonify({"ok": True, "restart_required": True, "changed_keys": changed})
|
||||
|
||||
@app.route("/api/admin/restart", methods=["POST"])
|
||||
@api_auth
|
||||
def api_admin_restart():
|
||||
result = restart_instance_pm2(exchange_key, defer=True)
|
||||
code = 200 if result.get("ok") else 500
|
||||
return jsonify({"ok": bool(result.get("ok")), **result}), code
|
||||
|
||||
@app.route("/api/admin/health", methods=["GET"])
|
||||
def api_admin_health():
|
||||
return jsonify({"ok": True, "status": "up"})
|
||||
|
||||
def tab_allowed_fn(tab: str) -> bool:
|
||||
prefs = get_display_prefs(get_db)
|
||||
return tab_allowed(tab, prefs)
|
||||
|
||||
app.config["INSTANCE_GET_DB"] = get_db
|
||||
app.config["INSTANCE_TAB_ALLOWED_FN"] = tab_allowed_fn
|
||||
|
||||
@app.route("/api/embed/tab_allowed/<tab>", methods=["GET"])
|
||||
@api_auth
|
||||
def api_tab_allowed(tab: str):
|
||||
prefs = get_display_prefs(get_db)
|
||||
return jsonify({"ok": True, "tab": tab, "allowed": tab_allowed(tab, prefs)})
|
||||
|
||||
|
||||
def merge_ui_template_context(page: str, get_db: Callable, **settings_kwargs: Any) -> dict[str, Any]:
|
||||
from lib.instance.instance_settings_lib import settings_page_context
|
||||
|
||||
prefs = get_display_prefs(get_db)
|
||||
ctx = {
|
||||
"display": prefs,
|
||||
"display_meta": display_meta_for_ui(),
|
||||
**settings_page_context(page, display=prefs, **settings_kwargs),
|
||||
}
|
||||
return ctx
|
||||
|
||||
@@ -268,7 +268,7 @@ function toggleListWindowCustom(){
|
||||
|
||||
function applyListWindow(){
|
||||
const qs = listWindowQueryString();
|
||||
const path = window.location.pathname || "/options";
|
||||
const path = window.location.pathname || "/trade";
|
||||
window.location.href = qs ? (path + "?" + qs) : path;
|
||||
}
|
||||
|
||||
@@ -1136,13 +1136,36 @@ function paintRealtimePnlFromSnapshot(data){
|
||||
}
|
||||
}
|
||||
|
||||
function formatOptionsFundingLabel(usdc, usdt) {
|
||||
// 期权侧顶栏仅 USDC;usdt 参数忽略(USDT 在永续资金/交易账户)
|
||||
function formatOptionsFundingLabel(usdc, usdt, eth, marginMode, underly) {
|
||||
if (usdc === null || usdc === undefined || usdc === "") return "—";
|
||||
const n = Number(usdc);
|
||||
if (Number.isNaN(n)) return "—";
|
||||
return `${n.toFixed(2)} USDC`;
|
||||
}
|
||||
function formatTradingAccountLabel(usdt, eth, btc, marginMode) {
|
||||
const mode = String(marginMode || "coin").toLowerCase();
|
||||
if (mode !== "coin") {
|
||||
if (usdt === null || usdt === undefined || usdt === "") return "—";
|
||||
const n = Number(usdt);
|
||||
if (Number.isNaN(n)) return "—";
|
||||
return `${n.toFixed(2)}U`;
|
||||
}
|
||||
const parts = [];
|
||||
if (usdt !== null && usdt !== undefined && usdt !== "") {
|
||||
const n = Number(usdt);
|
||||
if (!Number.isNaN(n)) parts.push(`${n.toFixed(2)} USDT`);
|
||||
}
|
||||
const pushCoin = (v, ccy) => {
|
||||
if (v === null || v === undefined || v === "") return;
|
||||
const n = Number(v);
|
||||
if (Number.isNaN(n) || !(n >= (ccy === "BTC" ? 1e-7 : 1e-6))) return;
|
||||
const txt = String(n.toFixed(6)).replace(/\.?0+$/, "");
|
||||
parts.push(`${txt || "0"} ${ccy}`);
|
||||
};
|
||||
pushCoin(eth, "ETH");
|
||||
pushCoin(btc, "BTC");
|
||||
return parts.length ? parts.join(" / ") : "—";
|
||||
}
|
||||
|
||||
function setFundsFieldText(field, text){
|
||||
if(text == null || text === "") return;
|
||||
@@ -1156,6 +1179,11 @@ function applyPerpFundsVisibility(show){
|
||||
el.style.display = on ? "" : "none";
|
||||
});
|
||||
}
|
||||
function applyOptionsFundsVisibility(show){
|
||||
document.querySelectorAll("[data-options-funds='1']").forEach((el) => {
|
||||
el.style.display = show ? "" : "none";
|
||||
});
|
||||
}
|
||||
function accountSnapshotFundingMissing(data){
|
||||
if(!data || typeof data !== "object") return true;
|
||||
if(data.show_perp_funds === false){
|
||||
@@ -1175,16 +1203,13 @@ function accountSnapshotFundingMissing(data){
|
||||
let accountSnapshotRetryCount = 0;
|
||||
function applyAccountSnapshot(data){
|
||||
if(!data || typeof data !== "object") return;
|
||||
if(data.updated_at){
|
||||
const updatedEl = document.getElementById("price-last-updated");
|
||||
if(updatedEl) updatedEl.innerText = data.updated_at;
|
||||
}
|
||||
const coinMode = String(data.options_margin_mode || "coin").toLowerCase() === "coin";
|
||||
if(typeof data.show_perp_funds !== "undefined"){
|
||||
applyPerpFundsVisibility(data.show_perp_funds);
|
||||
}
|
||||
if(data.exchange_mode_label){
|
||||
setFundsFieldText("exchange-mode-label", data.exchange_mode_label);
|
||||
applyPerpFundsVisibility(data.show_perp_funds !== false || coinMode);
|
||||
} else if (coinMode) {
|
||||
applyPerpFundsVisibility(true);
|
||||
}
|
||||
applyOptionsFundsVisibility(!coinMode);
|
||||
if(data.funding_usdt != null && data.funding_usdt !== ""){
|
||||
setFundsFieldText("total-capital", `${Number(data.funding_usdt).toFixed(2)}U`);
|
||||
}
|
||||
@@ -1192,14 +1217,34 @@ function applyAccountSnapshot(data){
|
||||
setFundsFieldText("total-funds", `${Number(data.total_funds).toFixed(2)}U`);
|
||||
}
|
||||
if(data.current_capital != null && data.current_capital !== "" && !Number.isNaN(Number(data.current_capital))){
|
||||
setFundsFieldText("current-capital", `${Number(data.current_capital).toFixed(2)}U`);
|
||||
setFundsFieldText(
|
||||
"current-capital",
|
||||
formatTradingAccountLabel(
|
||||
data.current_capital,
|
||||
data.options_trading_eth,
|
||||
data.options_trading_btc,
|
||||
data.options_margin_mode
|
||||
)
|
||||
);
|
||||
}
|
||||
if(data.options_funding_usdc != null || data.options_funding_usdt != null){
|
||||
const optFunding = formatOptionsFundingLabel(data.options_funding_usdc, data.options_funding_usdt);
|
||||
if(!coinMode && (data.options_funding_usdc != null || data.options_funding_usdt != null || data.options_funding_eth != null)){
|
||||
const optFunding = formatOptionsFundingLabel(
|
||||
data.options_funding_usdc,
|
||||
data.options_funding_usdt,
|
||||
data.options_funding_eth,
|
||||
data.options_margin_mode,
|
||||
data.options_underly
|
||||
);
|
||||
setFundsFieldText("options-funding-usdc", optFunding);
|
||||
}
|
||||
if(data.options_trading_usdc != null || data.options_trading_usdt != null){
|
||||
const optTrading = formatOptionsFundingLabel(data.options_trading_usdc, data.options_trading_usdt);
|
||||
if(!coinMode && (data.options_trading_usdc != null || data.options_trading_usdt != null || data.options_trading_eth != null)){
|
||||
const optTrading = formatOptionsFundingLabel(
|
||||
data.options_trading_usdc,
|
||||
data.options_trading_usdt,
|
||||
data.options_trading_eth,
|
||||
data.options_margin_mode,
|
||||
data.options_underly
|
||||
);
|
||||
setFundsFieldText("options-trading-usdc", optTrading);
|
||||
}
|
||||
if(typeof data.unrealized_pnl !== "undefined"){
|
||||
@@ -1270,11 +1315,7 @@ function applyAccountSnapshot(data){
|
||||
}
|
||||
function refreshAccountSnapshot(opts){
|
||||
const options = opts || {};
|
||||
const params = new URLSearchParams();
|
||||
if(options.force) params.set("force", "1");
|
||||
const page = (document.body && document.body.getAttribute("data-page")) || "";
|
||||
if(page) params.set("page", page);
|
||||
const qs = params.toString() ? ("?" + params.toString()) : "";
|
||||
const qs = options.force ? "?force=1" : "";
|
||||
fetch("/api/account_snapshot" + qs).then(r=>r.json()).then(data=>{
|
||||
applyAccountSnapshot(data);
|
||||
if(accountSnapshotFundingMissing(data) && !options.force && accountSnapshotRetryCount < 3){
|
||||
|
||||
+2101
-1850
File diff suppressed because it is too large
Load Diff
@@ -38,11 +38,13 @@
|
||||
{% include 'instance_header_stats.html' %}
|
||||
</div>
|
||||
<div class="instance-header-phone-strip instance-phone-only" aria-label="手机资金摘要">
|
||||
<span class="inst-phone-chip"{% if not (show_perp_funds|default(true)) %} style="display:none"{% endif %} data-perp-funds="1">
|
||||
{% set _coin_margin = (options_margin_mode|default('coin')) == 'coin' %}
|
||||
{% set _show_perp = (show_perp_funds|default(true)) or _coin_margin %}
|
||||
<span class="inst-phone-chip"{% if not _show_perp %} style="display:none"{% endif %} data-perp-funds="1">
|
||||
<em>交易</em>
|
||||
<b data-funds-field="current-capital">{{ funds_fmt(current_capital) }}U</b>
|
||||
<b data-funds-field="current-capital">{{ trading_account_label(current_capital, options_trading_eth, options_trading_btc, margin_mode=options_margin_mode|default('coin')) }}</b>
|
||||
</span>
|
||||
<span class="inst-phone-chip"{% if not (show_perp_funds|default(true)) %} style="display:none"{% endif %} data-perp-funds="1">
|
||||
<span class="inst-phone-chip"{% if not _show_perp %} style="display:none"{% endif %} data-perp-funds="1">
|
||||
<em>资金</em>
|
||||
<b data-funds-field="total-capital">{% if funding_usdt is not none %}{{ funds_fmt(funding_usdt) }}U{% else %}—{% endif %}</b>
|
||||
</span>
|
||||
|
||||
@@ -1,49 +1,51 @@
|
||||
{# 资金与统计条(顶栏 / 系统设置共用,单行展示) #}
|
||||
<div class="instance-header-stats{% if options_enabled %} instance-header-stats--options{% endif %}">
|
||||
<div class="stat-strip-item stat-strip-item--primary">
|
||||
<div class="label">交易所</div>
|
||||
<div class="value" id="exchange-mode-label" data-funds-field="exchange-mode-label">{{ exchange_display }}</div>
|
||||
</div>
|
||||
<div class="stat-strip-item">
|
||||
<div class="label">交易日</div>
|
||||
<div class="value">{{ trading_day }}</div>
|
||||
</div>
|
||||
<div class="stat-strip-item stat-strip-item--primary">
|
||||
<div class="label">总交易</div>
|
||||
<div class="value" id="stat-total" data-funds-field="stat-total">{{ total }}</div>
|
||||
</div>
|
||||
<div class="stat-strip-item">
|
||||
<div class="label">胜率</div>
|
||||
<div class="value" id="stat-rate" data-funds-field="stat-rate">{{ rate }}%</div>
|
||||
</div>
|
||||
<div class="stat-strip-item" title="平均盈利 ÷ 平均亏损(当前列表窗口)">
|
||||
<div class="label">盈亏比</div>
|
||||
<div class="value" id="stat-pl-ratio" data-funds-field="stat-pl-ratio">{% if profit_loss_ratio is not none %}{{ profit_loss_ratio }}{% else %}—{% endif %}</div>
|
||||
</div>
|
||||
<div class="stat-strip-item">
|
||||
<div class="label">总资金</div>
|
||||
<div class="value" id="total-funds" data-funds-field="total-funds">{% if total_funds is not none %}{{ funds_fmt(total_funds) }}U{% else %}—{% endif %}</div>
|
||||
</div>
|
||||
<div class="stat-strip-item"{% if not (show_perp_funds|default(true)) %} style="display:none"{% endif %} data-perp-funds="1">
|
||||
<div class="label">资金账户</div>
|
||||
<div class="value" id="total-capital" data-funds-field="total-capital">{% if funding_usdt is not none %}{{ funds_fmt(funding_usdt) }}U{% else %}—{% endif %}</div>
|
||||
</div>
|
||||
<div class="stat-strip-item"{% if not (show_perp_funds|default(true)) %} style="display:none"{% endif %} data-perp-funds="1">
|
||||
<div class="label">交易账户</div>
|
||||
<div class="value" id="current-capital" data-funds-field="current-capital">{{ funds_fmt(current_capital) }}U</div>
|
||||
</div>
|
||||
{% if options_enabled %}
|
||||
<div class="stat-strip-item">
|
||||
<div class="label">期权资金账户</div>
|
||||
<div class="value" id="options-funding-usdc" data-funds-field="options-funding-usdc">{{ options_funding_label(options_funding_usdc) }}</div>
|
||||
</div>
|
||||
<div class="stat-strip-item">
|
||||
<div class="label">期权交易账户</div>
|
||||
<div class="value" id="options-trading-usdc" data-funds-field="options-trading-usdc">{{ options_funding_label(options_trading_usdc) }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="stat-strip-item stat-strip-item--pnl">
|
||||
<div class="label">实时盈亏</div>
|
||||
<div class="value" id="realtime-pnl" data-funds-field="realtime-pnl">—</div>
|
||||
</div>
|
||||
</div>
|
||||
{# 资金与统计条(顶栏 / 系统设置共用,单行展示) #}
|
||||
{% set _coin_margin = (options_margin_mode|default('coin')) == 'coin' %}
|
||||
{% set _show_perp = (show_perp_funds|default(true)) or _coin_margin %}
|
||||
<div class="instance-header-stats{% if options_enabled %} instance-header-stats--options{% endif %}">
|
||||
<div class="stat-strip-item stat-strip-item--primary">
|
||||
<div class="label">交易所</div>
|
||||
<div class="value">{{ exchange_display }}</div>
|
||||
</div>
|
||||
<div class="stat-strip-item">
|
||||
<div class="label">交易日</div>
|
||||
<div class="value">{{ trading_day }}</div>
|
||||
</div>
|
||||
<div class="stat-strip-item stat-strip-item--primary">
|
||||
<div class="label">总交易</div>
|
||||
<div class="value" id="stat-total" data-funds-field="stat-total">{{ total }}</div>
|
||||
</div>
|
||||
<div class="stat-strip-item">
|
||||
<div class="label">胜率</div>
|
||||
<div class="value" id="stat-rate" data-funds-field="stat-rate">{{ rate }}%</div>
|
||||
</div>
|
||||
<div class="stat-strip-item" title="平均盈利 ÷ 平均亏损(当前列表窗口)">
|
||||
<div class="label">盈亏比</div>
|
||||
<div class="value" id="stat-pl-ratio" data-funds-field="stat-pl-ratio">{% if profit_loss_ratio is not none %}{{ profit_loss_ratio }}{% else %}—{% endif %}</div>
|
||||
</div>
|
||||
<div class="stat-strip-item">
|
||||
<div class="label">总资金</div>
|
||||
<div class="value" id="total-funds" data-funds-field="total-funds">{% if total_funds is not none %}{{ funds_fmt(total_funds) }}U{% else %}—{% endif %}</div>
|
||||
</div>
|
||||
<div class="stat-strip-item"{% if not _show_perp %} style="display:none"{% endif %} data-perp-funds="1">
|
||||
<div class="label">资金账户</div>
|
||||
<div class="value" id="total-capital" data-funds-field="total-capital">{% if funding_usdt is not none %}{{ funds_fmt(funding_usdt) }}U{% else %}—{% endif %}</div>
|
||||
</div>
|
||||
<div class="stat-strip-item"{% if not _show_perp %} style="display:none"{% endif %} data-perp-funds="1">
|
||||
<div class="label">交易账户</div>
|
||||
<div class="value" id="current-capital" data-funds-field="current-capital">{{ trading_account_label(current_capital, options_trading_eth, options_trading_btc, margin_mode=options_margin_mode|default('coin')) }}</div>
|
||||
</div>
|
||||
{% if options_enabled and not _coin_margin %}
|
||||
<div class="stat-strip-item" data-options-funds="1">
|
||||
<div class="label">期权资金账户</div>
|
||||
<div class="value" id="options-funding-usdc" data-funds-field="options-funding-usdc">{{ options_funding_label(options_funding_usdc, options_funding_usdt, options_funding_eth, options_margin_mode, options_underly|default('ETH')) }}</div>
|
||||
</div>
|
||||
<div class="stat-strip-item" data-options-funds="1">
|
||||
<div class="label">期权交易账户</div>
|
||||
<div class="value" id="options-trading-usdc" data-funds-field="options-trading-usdc">{{ options_funding_label(options_trading_usdc, options_trading_usdt, options_trading_eth, options_margin_mode, options_underly|default('ETH')) }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="stat-strip-item stat-strip-item--pnl">
|
||||
<div class="label">实时盈亏</div>
|
||||
<div class="value" id="realtime-pnl" data-funds-field="realtime-pnl">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user