diff --git a/.env.example b/.env.example index beacd56..4d12d86 100644 --- a/.env.example +++ b/.env.example @@ -72,8 +72,8 @@ TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true LIVE_TRADING_ENABLED=true # ============================================================================= -# 模拟资金(本地 SQLite 钱包; 系统设置可切换 模拟资金/实盘) -# 交易模式默认由系统设置(runtime trading.mode)切换; 也可启动时指定默认 +# 模拟资金(本地 SQLite 钱包; env「撮合模式」或系统设置可切换 sim/live) +# sim=本地模拟资金撮合; live=实盘账户 # ============================================================================= SIM_DEFAULT_MODE=sim SIM_INITIAL_EQUITY_USDT=10000 diff --git a/lib/env/env_schema.py b/lib/env/env_schema.py index 8882ca3..88f0150 100644 --- a/lib/env/env_schema.py +++ b/lib/env/env_schema.py @@ -99,6 +99,7 @@ HOT_RELOAD_EXACT = frozenset({ "OKX_OPTIONS_TRADE_BUDGET_USDC", "OKX_OPTIONS_BUDGET_BUFFER", "OKX_TRADE_MODE", + "SIM_DEFAULT_MODE", "MAX_ACTIVE_HEDGE_PLANS", "HEDGE_PLAN_LIVE_ORDER", "HEDGE_PLAN_OPTION_PRIMARY", @@ -159,6 +160,10 @@ SELECT_OPTIONS: dict[str, tuple[tuple[str, str], ...]] = { ("perp_options", "永期对冲"), ("options_options", "期期对冲"), ), + "SIM_DEFAULT_MODE": ( + ("sim", "模拟(sim)"), + ("live", "实盘(live)"), + ), "HEDGE_PLAN_OPTION_PRIMARY": ( ("true", "以期权为主"), ("false", "保险模式"), diff --git a/lib/env/env_ui_manifest.py b/lib/env/env_ui_manifest.py index 5bde533..2ab9163 100644 --- a/lib/env/env_ui_manifest.py +++ b/lib/env/env_ui_manifest.py @@ -19,7 +19,12 @@ from lib.env.env_schema import ( # 各所「交易所与实盘」字段(顺序即页面顺序) _EXCHANGE_LIVE_FIELDS: dict[str, list[tuple[str, str, str]]] = { "okx": [ - ("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程,不向交易所发单"), + ( + "SIM_DEFAULT_MODE", + "撮合模式", + "sim=本地模拟资金(公开行情撮合,不下真实单); live=实盘账户.保存后立即切换", + ), + ("LIVE_TRADING_ENABLED", "开启实盘下单", "仅 live 模式下生效;关闭时即使 live 也不向交易所发单"), ("OKX_API_KEY", "API Key", "账户 API(永续+期权共用)"), ("OKX_API_SECRET", "API Secret", "账户 API(永续+期权共用)"), ("OKX_API_PASSPHRASE", "API Passphrase", "OKX 必填"), @@ -291,6 +296,7 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = { "OKX_OPTIONS_MAX_DTE_DAYS": "2", "OKX_OPTIONS_MAX_ACTIVE_POSITIONS": "0", "OKX_TRADE_MODE": "options", + "SIM_DEFAULT_MODE": "sim", "MAX_ACTIVE_HEDGE_PLANS": "1", "HEDGE_PLAN_OO_CLOSE_MODE_ENABLED": "true", "HEDGE_PLAN_OO_BIAS_SPLIT_BY": "budget", @@ -303,6 +309,16 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = { def _effective_env_value(key: str, file_values: dict[str, str], schema_default: str = "") -> str: + if key == "SIM_DEFAULT_MODE": + # 展示当前生效撮合模式(运行时 trading.mode 优先) + try: + from lib.sim.mode_lib import peek_persisted_trading_mode + + cur = peek_persisted_trading_mode() + if cur: + return cur + except Exception: + pass if key == "OKX_TRADE_MODE": # 展示值必须与运行时 get_okx_trade_mode() 一致,避免未写入时默认 options 静默改模式 file_val = str(file_values.get(key) or "").strip() if key in file_values else "" diff --git a/lib/instance/runtime_config_lib.py b/lib/instance/runtime_config_lib.py index 43ea963..98725c3 100644 --- a/lib/instance/runtime_config_lib.py +++ b/lib/instance/runtime_config_lib.py @@ -51,12 +51,26 @@ def apply_env_reload(env_path: str, get_db: Callable, changed_keys: list[str], g field_map[field["key"]] = field for key in changed_keys: meta = field_map.get(key) or {} - if meta.get("hot_reload") and not meta.get("restart_required"): + # SIM_DEFAULT_MODE 不在 example schema 的 hot 列表时仍应热切 + is_hot = bool(meta.get("hot_reload") and not meta.get("restart_required")) + if key == "SIM_DEFAULT_MODE": + is_hot = True + if is_hot: val = os.getenv(key) if val is not None: hot[key] = val if hot: set_config_overrides(get_db, hot) + # 撮合模式:写入 .env 的同时切换运行时 trading.mode(sim|live) + if "SIM_DEFAULT_MODE" in changed_keys: + try: + from lib.sim.mode_lib import set_trading_mode + + raw = (os.getenv("SIM_DEFAULT_MODE") or "").strip().lower() + if raw in ("sim", "live"): + set_trading_mode(get_db, raw) + except Exception: + pass from lib.env.env_schema import updates_need_restart return {"restart_required": updates_need_restart(groups, changed_keys)} diff --git a/lib/sim/mode_lib.py b/lib/sim/mode_lib.py index 46056a1..3ac7d8b 100644 --- a/lib/sim/mode_lib.py +++ b/lib/sim/mode_lib.py @@ -20,6 +20,34 @@ def default_trading_mode() -> str: return MODE_SIM +def peek_persisted_trading_mode(db_path: str | None = None) -> str | None: + """只读 SQLite 中的 trading.mode(供 env 配置页展示当前生效值).""" + import sqlite3 + + path = (db_path or os.getenv("DB_PATH") or "crypto.db").strip() + if not path: + return None + if not os.path.isabs(path): + path = os.path.abspath(path) + if not os.path.isfile(path): + return None + try: + conn = sqlite3.connect(path, timeout=2) + try: + row = conn.execute( + "SELECT value FROM app_runtime_settings WHERE key=?", + (TRADING_MODE_KEY,), + ).fetchone() + finally: + conn.close() + except Exception: + return None + if not row: + return None + m = str(row[0] or "").strip().lower() + return m if m in VALID_MODES else None + + def normalize_mode(mode: str | None) -> str: m = (mode or "").strip().lower() if m in VALID_MODES: