Hide manual trade by default, block open while running, auto-refresh auth token.

Also fix flat-side reconcile to check both long and short residuals; document in 更新说明.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-26 22:43:38 +08:00
parent 0cf3756b09
commit 16efa44ffb
13 changed files with 304 additions and 38 deletions
+16
View File
@@ -89,6 +89,22 @@ async def me(
}
@router.post("/refresh", response_model=LoginResponse)
async def refresh_token(
username: Annotated[str, Depends(require_user)],
settings: Annotated[Settings, Depends(get_settings)],
) -> LoginResponse:
"""用仍有效的 Bearer 换发新 HMAC token(自动轮换,无需重登)。"""
token, ttl = issue_token(username, settings)
return LoginResponse(
token=token,
username=username,
expires_in=ttl,
env_name=settings.env_name,
mode=settings.mode,
)
@router.post("/change-credentials", response_model=LoginResponse)
async def change_credentials(
body: ChangeCredentialsRequest,
+5
View File
@@ -44,6 +44,7 @@ KEYS = (
"close_bid_mark_max_pct",
"perp_qty_eth",
"option_qty_eth",
"show_manual_trade_buttons",
)
@@ -65,6 +66,7 @@ class StrategySettingsBody(BaseModel):
close_bid_mark_max_pct: float | None = Field(default=None, ge=1, le=100)
perp_qty_eth: float | None = Field(default=None, ge=0.01, le=100)
option_qty_eth: float | None = Field(default=None, ge=0.01, le=100)
show_manual_trade_buttons: bool | None = None
exchange: str | None = Field(default=None, pattern="^(okx|binance|bn)$")
@@ -139,6 +141,9 @@ def _read_settings() -> dict:
"option_qty_eth": float(
db.get_setting("option_qty_eth", str(s.option_qty_eth)) or s.option_qty_eth
),
"show_manual_trade_buttons": _as_bool(
db.get_setting("show_manual_trade_buttons", "0"), False
),
"exchange": rt.exchange,
"perp_inst_id": rt.perp_inst_id,
"option_inst_family": rt.option_inst_family,
+18
View File
@@ -42,6 +42,19 @@ async def sim_open_group(
ok, reason = live_ready()
if not get_settings().is_sim and not ok:
raise HTTPException(status_code=400, detail=reason)
from ..strategy import get_engine
st = get_engine().state()
if st.get("running"):
raise HTTPException(
status_code=409,
detail="策略自动运行中,禁止手动开仓;请先暂停",
)
if not Ledger().get_setting_bool("show_manual_trade_buttons", False):
raise HTTPException(
status_code=403,
detail="未开启「显示手动开仓」;请在策略设置中开启后再用",
)
ex = get_executor()
if ex.has_open_position():
raise HTTPException(status_code=409, detail="有未平仓,禁止开下一组")
@@ -115,6 +128,11 @@ async def sim_open_group(
@router.post("/close-group")
async def sim_close_group(_user: Annotated[str, Depends(require_user)]) -> dict:
if not Ledger().get_setting_bool("show_manual_trade_buttons", False):
raise HTTPException(
status_code=403,
detail="未开启「显示手动开仓」;请在策略设置中开启后再用",
)
r = get_executor().close_group(reason="manual")
if not r.ok and not r.liquidity_wait:
raise HTTPException(status_code=400, detail=r.detail)
+12 -5
View File
@@ -76,12 +76,19 @@ def assert_safe_to_open_live(executor) -> tuple[bool, str]:
return False, "无法核对交易所持仓"
perp_inst = resolve_perp_inst_id(executor.db)
perp_side = str(pos.get("perp_side") or "long")
ex_sz = exchange_perp_abs_size(client, ex_name or "", perp_inst, perp_side)
if ex_sz is None:
return False, "无法核对交易所持仓"
# flat/opening:两侧都查,避免只查默认 long 漏掉 short 残留
if st in ("flat", "", "opening"):
sides = ("long", "short")
else:
sides = (str(pos.get("perp_side") or "long"),)
total = 0.0
for side in sides:
ex_sz = exchange_perp_abs_size(client, ex_name or "", perp_inst, side)
if ex_sz is None:
return False, "无法核对交易所持仓"
total += float(ex_sz)
if ex_sz > _PERP_EPS and st in ("flat", "", "opening"):
if total > _PERP_EPS and st in ("flat", "", "opening"):
return (
False,
"交易所有永续仓但本地无持仓,禁止新开,请人工核对",
+3
View File
@@ -128,6 +128,9 @@ class StrategyEngine:
"sim": s.is_sim,
"live_ready": (live_ready()[0] if not s.is_sim else True),
"live_ready_reason": (live_ready()[1] if not s.is_sim else "sim"),
"show_manual_trade_buttons": self.ledger.get_setting_bool(
"show_manual_trade_buttons", False
),
}
def _set_state(self, **kwargs: Any) -> None:
+20
View File
@@ -0,0 +1,20 @@
from __future__ import annotations
from app.api.auth import issue_token, verify_token
from app.config import get_settings
def test_issue_and_verify_token_roundtrip():
s = get_settings()
token, ttl = issue_token("admin", s)
assert ttl == s.auth_token_ttl_sec
assert verify_token(token, s) == "admin"
def test_refresh_mints_another_valid_token():
s = get_settings()
t1, _ = issue_token("admin", s)
t2, ttl = issue_token("admin", s)
assert ttl > 0
assert verify_token(t1, s) == "admin"
assert verify_token(t2, s) == "admin"