diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 8700d90..03693a9 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -7239,6 +7239,7 @@ def render_main_page(page="trade", embed_mode=None): orphan_live_positions = list_orphan_live_positions(conn) conn.close() from lib.instance.instance_embed_lib import embed_context_extras + from lib.instance.instance_settings_lib import settings_page_context template_ctx = dict( page=page, @@ -7315,6 +7316,14 @@ def render_main_page(page="trade", embed_mode=None): kline_timeframe=KLINE_TIMEFRAME, **strategy_extra, **embed_context_extras("binance"), + **settings_page_context( + page, + exchange_key="binance", + exchange_display=EXCHANGE_DISPLAY_NAME, + risk_status=risk_status, + trade_policy=TRADE_POLICY, + data_export_version=3, + ), **force_close_template_context( FORCE_CLOSE_ENABLED, FORCE_CLOSE_BJ_HOUR, @@ -7358,6 +7367,12 @@ def stats_page(): return render_main_page("stats") +@app.route("/settings") +@login_required +def settings_page(): + return render_main_page("settings") + + @app.route("/api/account_snapshot") @login_required def api_account_snapshot(): @@ -9615,7 +9630,7 @@ def manual_transfer(): amount = float(request.form.get("amount", "0")) except Exception: flash("划转金额格式错误") - return redirect("/") + return redirect("/settings") from_account = (request.form.get("from_account") or AUTO_TRANSFER_FROM).strip() to_account = (request.form.get("to_account") or AUTO_TRANSFER_TO).strip() ok, msg, _ = execute_transfer_usdt(amount, from_account, to_account) @@ -9630,7 +9645,7 @@ def manual_transfer(): flash(f"手动划转成功:{amount}U {from_account}->{to_account}") else: flash(f"手动划转失败:{msg}") - return redirect("/") + return redirect("/settings") def _journal_ai_chart_builder(row): diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 17739ff..a59e041 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -7033,6 +7033,7 @@ def render_main_page(page="trade", embed_mode=None): ) conn.close() from lib.instance.instance_embed_lib import embed_context_extras + from lib.instance.instance_settings_lib import settings_page_context template_ctx = dict( page=page, @@ -7118,6 +7119,14 @@ def render_main_page(page="trade", embed_mode=None): has_active_positions=bool(order_list), ), **embed_context_extras("gate"), + **settings_page_context( + page, + exchange_key="gate", + exchange_display=EXCHANGE_DISPLAY_NAME, + risk_status=risk_status, + trade_policy=TRADE_POLICY, + data_export_version=3, + ), ) if embed_mode == "fragment": return render_template("embed_page_fragment.html", **template_ctx) @@ -7173,6 +7182,12 @@ def stats_page(): return render_main_page("stats") +@app.route("/settings") +@login_required +def settings_page(): + return render_main_page("settings") + + @app.route("/api/account_snapshot") @login_required def api_account_snapshot(): @@ -9487,7 +9502,7 @@ def manual_transfer(): amount = float(request.form.get("amount", "0")) except Exception: flash("划转金额格式错误") - return redirect("/") + return redirect("/settings") from_account = (request.form.get("from_account") or AUTO_TRANSFER_FROM).strip() to_account = (request.form.get("to_account") or AUTO_TRANSFER_TO).strip() ok, msg, _ = execute_transfer_usdt(amount, from_account, to_account) @@ -9502,7 +9517,7 @@ def manual_transfer(): flash(f"手动划转成功:{amount}U {from_account}->{to_account}") else: flash(f"手动划转失败:{msg}") - return redirect(request.referrer or "/trade") + return redirect("/settings") def _journal_ai_chart_builder(row): diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index c953337..528ffa5 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -6583,6 +6583,7 @@ def render_main_page(page="trade", embed_mode=None): ) conn.close() from lib.instance.instance_embed_lib import embed_context_extras + from lib.instance.instance_settings_lib import settings_page_context template_ctx = dict( page=page, @@ -6661,6 +6662,14 @@ def render_main_page(page="trade", embed_mode=None): exchange_pnl_sync=exchange_pnl_sync, **strategy_extra, **embed_context_extras("okx"), + **settings_page_context( + page, + exchange_key="okx", + exchange_display=EXCHANGE_DISPLAY_NAME, + risk_status=risk_status, + trade_policy=TRADE_POLICY, + data_export_version=3, + ), **force_close_template_context( FORCE_CLOSE_ENABLED, FORCE_CLOSE_BJ_HOUR, @@ -6717,6 +6726,12 @@ def stats_page(): return render_main_page("stats") +@app.route("/settings") +@login_required +def settings_page(): + return render_main_page("settings") + + @app.route("/api/account_snapshot") @login_required def api_account_snapshot(): @@ -8999,7 +9014,7 @@ def manual_transfer(): amount = float(request.form.get("amount", "0")) except Exception: flash("划转金额格式错误") - return redirect("/") + return redirect("/settings") from_account = (request.form.get("from_account") or AUTO_TRANSFER_FROM).strip() to_account = (request.form.get("to_account") or AUTO_TRANSFER_TO).strip() ok, msg, _ = execute_transfer_usdt(amount, from_account, to_account) @@ -9014,7 +9029,7 @@ def manual_transfer(): flash(f"手动划转成功:{amount}U {from_account}->{to_account}") else: flash(f"手动划转失败:{msg}") - return redirect("/") + return redirect("/settings") def _journal_ai_chart_builder(row): diff --git a/lib/common/static/instance_embed.js b/lib/common/static/instance_embed.js index 59ede06..b3deb0a 100644 --- a/lib/common/static/instance_embed.js +++ b/lib/common/static/instance_embed.js @@ -9,6 +9,7 @@ strategy_records: "/strategy/records", records: "/records", stats: "/stats", + settings: "/settings", }; let navToken = 0; diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index 21b3a28..27edf15 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -1917,3 +1917,136 @@ html[data-theme="light"] .journal-detail-img-thumb { background: #eef2f7; } +/* ── 系统设置页 ── */ +.settings-page { + margin-top: 12px; +} + +.settings-cards-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 16px; + align-items: start; +} + +.settings-card h2 { + margin: 0 0 10px; + font-size: 1.05rem; +} + +.settings-card-desc, +.settings-env-hint, +.settings-transfer-auto { + color: var(--muted, #8892b0); + font-size: 0.82rem; + line-height: 1.5; + margin: 0 0 12px; +} + +.settings-live-status { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + font-size: 0.88rem; + margin: 0 0 10px; +} + +.settings-status-reason, +.settings-policy-note { + color: var(--muted, #8892b0); + font-size: 0.8rem; +} + +.settings-section { + margin-top: 14px; + padding-top: 12px; + border-top: 1px solid var(--border-soft, #2a3150); +} + +.settings-section h3 { + margin: 0 0 8px; + font-size: 0.92rem; + color: #cfd3ef; +} + +.settings-kv { + margin: 0; +} + +.settings-kv-row { + display: grid; + grid-template-columns: 9.5em 1fr; + gap: 8px 12px; + padding: 6px 0; + font-size: 0.82rem; + border-bottom: 1px dashed rgba(136, 146, 176, 0.15); +} + +.settings-kv-row:last-child { + border-bottom: none; +} + +.settings-kv-row dt { + margin: 0; + color: #9aa3c7; +} + +.settings-kv-row dd { + margin: 0; +} + +.settings-kv-value { + color: #e8ecff; + font-weight: 600; +} + +.settings-kv-note { + display: block; + margin-top: 2px; + color: #8892b0; + font-size: 0.75rem; + font-weight: 400; +} + +.settings-link-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.settings-export-link { + display: block; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid var(--border-soft, #2a3150); + background: var(--inset-surface, #12151f); + color: #8fc8ff; + text-decoration: none; + font-size: 0.88rem; +} + +.settings-export-link:hover { + border-color: #3d4f7a; + background: #1a2030; +} + +.settings-transfer-form { + margin-top: 10px; +} + +html[data-theme="light"] .settings-section { + border-top-color: #d0dae4; +} + +html[data-theme="light"] .settings-section h3, +html[data-theme="light"] .settings-kv-value { + color: #142232; +} + +html[data-theme="light"] .settings-export-link { + background: #fff; + border-color: #b8c8d8; + color: #1d4f8c; +} + diff --git a/lib/instance/instance_embed_context_lib.py b/lib/instance/instance_embed_context_lib.py index c895c02..803e29b 100644 --- a/lib/instance/instance_embed_context_lib.py +++ b/lib/instance/instance_embed_context_lib.py @@ -36,10 +36,11 @@ def embed_render_plan(page: str, embed_mode: str | None) -> EmbedRenderPlan: ) is_shell = embed_mode == "shell" is_strategy = page in EMBED_STRATEGY_PAGES + is_settings = page == "settings" return EmbedRenderPlan( exchange_capitals=is_shell, records_rows=page == "records", - records_summary=is_shell and page != "records", + records_summary=is_shell and page != "records" and not is_settings, key_history=page == "key_monitor", key_list=page in ("key_monitor", "trade") or is_strategy, orders=page == "trade" or is_strategy, diff --git a/lib/instance/instance_embed_lib.py b/lib/instance/instance_embed_lib.py index 036a56a..84e96fd 100644 --- a/lib/instance/instance_embed_lib.py +++ b/lib/instance/instance_embed_lib.py @@ -17,6 +17,7 @@ EMBED_TABS: tuple[str, ...] = ( "strategy_records", "records", "stats", + "settings", ) PATH_TO_EMBED_TAB: dict[str, str] = { @@ -29,6 +30,7 @@ PATH_TO_EMBED_TAB: dict[str, str] = { "/strategy/records": "strategy_records", "/records": "records", "/stats": "stats", + "/settings": "settings", } ORDER_RULE_TIPS_BY_EXCHANGE: dict[str, str] = { diff --git a/lib/instance/instance_settings_lib.py b/lib/instance/instance_settings_lib.py new file mode 100644 index 0000000..d3d689f --- /dev/null +++ b/lib/instance/instance_settings_lib.py @@ -0,0 +1,162 @@ +"""实例「系统设置」页:从 .env 汇总风控说明(三所共用)。""" +from __future__ import annotations + +import os +from typing import Any, Optional + +from lib.key_monitor.key_auto_order_lib import load_key_auto_order_enabled +from lib.trade.account_risk_lib import ( + cooling_hours_manual, + cooling_hours_manual_journal, + manual_close_daily_limit, + max_active_positions_from_env, + mood_issues_daily_freeze_enabled, + risk_control_enabled, +) +from lib.trade.position_sizing_lib import is_full_margin_mode, load_position_sizing_mode, mode_label_zh +from lib.trade.trade_policy_lib import TradePolicy + + +def _env_bool(key: str, default: bool = False) -> bool: + raw = (os.getenv(key) or "").strip().lower() + if not raw: + return default + return raw in ("1", "true", "yes", "on") + + +def _env_float(key: str, default: float) -> float: + try: + return float(os.getenv(key, str(default))) + except (TypeError, ValueError): + return default + + +def _env_int(key: str, default: int) -> int: + try: + return int(os.getenv(key, str(default))) + except (TypeError, ValueError): + return default + + +def _row(label: str, value: str, note: str = "") -> dict[str, str]: + return {"label": label, "value": value, "note": note} + + +def _on_off(enabled: bool) -> str: + return "开启" if enabled else "关闭" + + +def build_instance_settings_view( + *, + exchange_key: str, + exchange_display: str, + risk_status: Optional[dict[str, Any]] = None, + trade_policy: Optional[TradePolicy] = None, + data_export_version: int = 3, +) -> dict[str, Any]: + rs = risk_status or {} + sizing_mode = load_position_sizing_mode() + key_auto = load_key_auto_order_enabled() + reset_hour = _env_int("TRADING_DAY_RESET_HOUR", 8) + hard_limit = _env_int("DAILY_OPEN_HARD_LIMIT", 0) + alert_threshold = _env_int("DAILY_OPEN_ALERT_THRESHOLD", 5) + force_close_on = _env_bool("FORCE_CLOSE_ENABLED", False) + force_close_hour = _env_int("FORCE_CLOSE_BJ_HOUR", 0) + auto_transfer_on = _env_bool("AUTO_TRANSFER_ENABLED", False) + + sections: list[dict[str, Any]] = [] + + sections.append( + { + "title": "交易执行", + "rows": [ + _row("最大同时持仓", str(max_active_positions_from_env())), + _row("计仓模式", mode_label_zh(sizing_mode)), + _row("以损定仓风险%", f"{_env_float('RISK_PERCENT', 2):g}%"), + _row("人工最低盈亏比", f">= {_env_float('MANUAL_MIN_PLANNED_RR', 1.4):g}:1"), + _row( + "交易日切点", + f"北京时间 {reset_hour}:00", + "新交易日统计与部分开仓限制以此为准", + ), + _row( + "单日开仓提醒", + f"第 {alert_threshold} 次", + "达次数推送企业微信,不拦单", + ), + _row( + "单日开仓硬上限", + str(hard_limit) if hard_limit > 0 else "未启用", + "达上限后禁止一切新开仓直至下一交易日" if hard_limit > 0 else "", + ), + ], + } + ) + + sections.append( + { + "title": "账户冷静期", + "rows": [ + _row("风控总开关", _on_off(risk_control_enabled())), + _row("手动平仓冷静", f"{cooling_hours_manual():g} 小时"), + _row("复盘后冷静", f"{cooling_hours_manual_journal():g} 小时", "手动平仓且填写说明后可缩短"), + _row("日手动平仓上限", f"{manual_close_daily_limit()} 次", "超限当日冻结"), + _row( + "复盘情绪日冻结", + _on_off(mood_issues_daily_freeze_enabled()), + "复盘勾选心态标签可触发当日冻结", + ), + ], + } + ) + + key_rows = [ + _row("关键位自动单", _on_off(key_auto)), + _row("关键位最低盈亏比", f"> {_env_float('KEY_AUTO_MIN_PLANNED_RR', 1.5):g}:1"), + ] + if is_full_margin_mode(sizing_mode): + key_rows.append( + _row( + "全仓模式", + "仅触价类自动单", + "箱体/斐波等自动开仓在全仓下禁用", + ) + ) + sections.append({"title": "关键位与自动单", "rows": key_rows}) + + if force_close_on or (exchange_key or "").strip().lower() == "gate": + sections.append( + { + "title": "整点强制清仓", + "rows": [ + _row("强制清仓", _on_off(force_close_on)), + _row("执行时刻", f"北京时间 {force_close_hour}:00 起 15 分钟内"), + ], + } + ) + + policy_note = "" + if trade_policy and getattr(trade_policy, "badge_text", ""): + policy_note = str(trade_policy.badge_text) + + return { + "exchange_display": exchange_display, + "risk_status_label": str(rs.get("status_label") or "正常"), + "risk_status_reason": str(rs.get("reason") or "").strip(), + "can_trade": bool(rs.get("can_trade", True)), + "trade_policy_note": policy_note, + "sections": sections, + "data_export_version": int(data_export_version), + "show_transfer": (exchange_key or "").strip().lower() in ("gate", "binance", "okx"), + "auto_transfer_enabled": auto_transfer_on, + "auto_transfer_bj_hour": _env_int("AUTO_TRANSFER_BJ_HOUR", 8), + "auto_transfer_amount": _env_float("AUTO_TRANSFER_AMOUNT", 30), + "auto_transfer_from": (os.getenv("AUTO_TRANSFER_FROM") or "funding").strip(), + "auto_transfer_to": (os.getenv("AUTO_TRANSFER_TO") or "swap").strip(), + } + + +def settings_page_context(page: str, **kwargs: Any) -> dict[str, Any]: + if (page or "").strip() != "settings": + return {} + return {"instance_settings": build_instance_settings_view(**kwargs)} diff --git a/lib/instance/templates/embed_page_fragment.html b/lib/instance/templates/embed_page_fragment.html index c71989d..0f5f453 100644 --- a/lib/instance/templates/embed_page_fragment.html +++ b/lib/instance/templates/embed_page_fragment.html @@ -390,6 +390,9 @@ {% endif %} + {% if page == 'settings' %} + {% include 'settings_panel.html' %} + {% endif %} {% if page == 'stats' %}