Add SIM/LIVE switch with API keys saved to .env and OKX live executor.

Enable settings UI for mode/keys, gate strategy start when LIVE is not ready, and stop PM2 from forcing MODE=SIM.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-26 21:08:39 +08:00
parent c2113b1a57
commit e666230d0b
19 changed files with 1367 additions and 33 deletions
+115 -1
View File
@@ -1,11 +1,18 @@
from __future__ import annotations
from typing import Annotated
from typing import Annotated, Literal
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from ..config import get_settings
from ..env_store import (
binance_keys_configured,
live_ready,
mask_secret,
okx_keys_configured,
upsert_env_keys,
)
from ..exchange.runtime import (
load_runtime_settings,
normalize_exchange_name,
@@ -195,3 +202,110 @@ async def put_strategy_settings(
) from e
return _read_settings()
class RuntimeSettingsBody(BaseModel):
mode: Literal["SIM", "LIVE"] | None = None
confirm_live: bool | None = False
okx_api_key: str | None = None
okx_api_secret: str | None = None
okx_api_passphrase: str | None = None
binance_api_key: str | None = None
binance_api_secret: str | None = None
def _runtime_payload() -> dict:
s = get_settings()
rt = load_runtime_settings()
mode = "SIM" if s.is_sim else "LIVE"
ready, reason = live_ready(exchange=rt.exchange)
return {
"mode": mode,
"exchange": rt.exchange,
"okx_configured": okx_keys_configured(s),
"binance_configured": binance_keys_configured(s),
"okx_api_key_masked": mask_secret(s.okx_api_key),
"okx_api_secret_masked": mask_secret(s.okx_api_secret),
"okx_api_passphrase_masked": mask_secret(s.okx_api_passphrase),
"binance_api_key_masked": mask_secret(s.binance_api_key),
"binance_api_secret_masked": mask_secret(s.binance_api_secret),
"live_ready": bool(ready) if mode == "LIVE" else True,
"live_ready_reason": reason if mode == "LIVE" else "sim",
"sim": s.is_sim,
}
@router.get("/runtime")
async def get_runtime_settings(_user: Annotated[str, Depends(require_user)]) -> dict:
return _runtime_payload()
@router.put("/runtime")
async def put_runtime_settings(
body: RuntimeSettingsBody,
_user: Annotated[str, Depends(require_user)],
) -> dict:
db = get_db()
s = get_settings()
cur_mode = "SIM" if s.is_sim else "LIVE"
new_mode = (body.mode or cur_mode).strip().upper()
if new_mode not in ("SIM", "LIVE"):
raise HTTPException(status_code=400, detail="mode 须为 SIM 或 LIVE")
if new_mode != cur_mode and Matcher(db).has_open_position():
raise HTTPException(
status_code=409,
detail="有未平仓,无法切换 SIM/LIVE;请先平仓后再改",
)
if new_mode == "LIVE" and cur_mode != "LIVE":
if not body.confirm_live:
raise HTTPException(
status_code=400,
detail="切换到 LIVE 须二次确认(confirm_live=true",
)
updates: dict[str, str] = {}
if body.okx_api_key is not None and body.okx_api_key.strip():
updates["OKX_API_KEY"] = body.okx_api_key.strip()
if body.okx_api_secret is not None and body.okx_api_secret.strip():
updates["OKX_API_SECRET"] = body.okx_api_secret.strip()
if body.okx_api_passphrase is not None and body.okx_api_passphrase.strip():
updates["OKX_API_PASSPHRASE"] = body.okx_api_passphrase.strip()
if body.binance_api_key is not None and body.binance_api_key.strip():
updates["BINANCE_API_KEY"] = body.binance_api_key.strip()
if body.binance_api_secret is not None and body.binance_api_secret.strip():
updates["BINANCE_API_SECRET"] = body.binance_api_secret.strip()
if new_mode != cur_mode:
updates["MODE"] = new_mode
if updates:
upsert_env_keys(updates)
s2 = get_settings()
if new_mode == "LIVE":
rt = load_runtime_settings()
if rt.exchange == "okx" and not okx_keys_configured(s2):
if cur_mode == "SIM":
upsert_env_keys({"MODE": "SIM"})
raise HTTPException(
status_code=400,
detail="切到 LIVE 前请先配置完整 OKX API Key/Secret/Passphrase",
)
if rt.exchange == "binance" and not binance_keys_configured(s2):
if cur_mode == "SIM":
upsert_env_keys({"MODE": "SIM"})
raise HTTPException(
status_code=400,
detail="切到 LIVE 前请先配置完整币安 API Key/Secret",
)
try:
from ..strategy import get_engine
get_engine().refresh_executor()
except Exception:
pass
return _runtime_payload()
+10 -5
View File
@@ -6,10 +6,11 @@ from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from ..config import get_settings
from ..env_store import live_ready
from ..live import get_executor
from ..market import get_gateway
from ..models.db import get_db
from ..sim.ledger import Ledger
from ..sim.matcher import Matcher
from ..strategy.clock import can_open_new, window_key
from ..strategy.group import next_group_id
from .auth import require_user
@@ -29,7 +30,7 @@ async def sim_ledger(_user: Annotated[str, Depends(require_user)]) -> dict:
@router.get("/position")
async def sim_position(_user: Annotated[str, Depends(require_user)]) -> dict:
m = Matcher()
m = get_executor()
return {"position": m.current_position(), "unrealized": m.unrealized()}
@@ -38,7 +39,11 @@ async def sim_open_group(
_user: Annotated[str, Depends(require_user)],
body: ManualOpenBody | None = None,
) -> dict:
if Matcher().has_open_position():
ok, reason = live_ready()
if not get_settings().is_sim and not ok:
raise HTTPException(status_code=400, detail=reason)
ex = get_executor()
if ex.has_open_position():
raise HTTPException(status_code=409, detail="有未平仓,禁止开下一组")
s = get_settings()
skip_weekends = Ledger().get_setting_bool("skip_weekends", s.skip_weekends)
@@ -85,7 +90,7 @@ async def sim_open_group(
db.fetchall("SELECT group_id FROM groups WHERE group_id LIKE ?", (f"G-{wkey}-%",))
)
gid = next_group_id(count)
r = Matcher().open_group(
r = ex.open_group(
group_id=gid,
bias=bias,
option_side=option_side,
@@ -110,7 +115,7 @@ async def sim_open_group(
@router.post("/close-group")
async def sim_close_group(_user: Annotated[str, Depends(require_user)]) -> dict:
r = Matcher().close_group(reason="manual")
r = get_executor().close_group(reason="manual")
if not r.ok and not r.liquidity_wait:
raise HTTPException(status_code=400, detail=r.detail)
return {