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:
+115
-1
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user