ee0ec57f89
Co-authored-by: Cursor <cursoragent@cursor.com>
190 lines
6.6 KiB
Python
190 lines
6.6 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
|
|
from ..config import get_settings
|
|
from ..exchange.runtime import (
|
|
load_runtime_settings,
|
|
normalize_exchange_name,
|
|
persist_exchange_choice,
|
|
reload_market_session,
|
|
)
|
|
from ..models.db import get_db
|
|
from ..sim.ledger import Ledger
|
|
from ..sim.matcher import Matcher
|
|
from .auth import require_user
|
|
|
|
router = APIRouter(prefix="/api/settings", tags=["settings"])
|
|
|
|
KEYS = (
|
|
"fee_rate",
|
|
"exit_move_pct",
|
|
"exit_mode",
|
|
"net_profit_target",
|
|
"premium_exit_multiple",
|
|
"rest_seconds",
|
|
"skip_weekends",
|
|
"initial_equity",
|
|
"leverage",
|
|
"min_option_hours",
|
|
"min_option_leverage",
|
|
"max_atm_open_offset",
|
|
"close_bid_mark_max_pct",
|
|
"perp_qty_eth",
|
|
"option_qty_eth",
|
|
)
|
|
|
|
|
|
class StrategySettingsBody(BaseModel):
|
|
fee_rate: float | None = Field(default=None, ge=0, le=0.05)
|
|
exit_move_pct: float | None = Field(default=None, ge=0.1, le=50)
|
|
exit_mode: str | None = Field(default=None, pattern="^(fixed_usdt|premium_multiple)$")
|
|
net_profit_target: float | None = Field(default=None, ge=0.1, le=1_000_000)
|
|
premium_exit_multiple: float | None = Field(default=None, ge=0.1, le=100)
|
|
rest_seconds: int | None = Field(default=None, ge=0, le=3600)
|
|
skip_weekends: bool | None = None
|
|
initial_equity: float | None = Field(default=None, ge=1000, le=10_000_000)
|
|
leverage: float | None = Field(default=None, ge=1, le=125)
|
|
min_option_hours: float | None = Field(default=None, ge=1, le=720)
|
|
min_option_leverage: float | None = Field(default=None, ge=1, le=10000)
|
|
max_atm_open_offset: float | None = Field(default=None, ge=0, le=100)
|
|
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)
|
|
exchange: str | None = Field(default=None, pattern="^(okx|binance|bn)$")
|
|
|
|
|
|
def _as_bool(raw: str | None, default: bool) -> bool:
|
|
if raw is None or raw == "":
|
|
return default
|
|
return str(raw).strip().lower() in ("1", "true", "yes", "on")
|
|
|
|
|
|
def _read_settings() -> dict:
|
|
db = get_db()
|
|
s = get_settings()
|
|
rt = load_runtime_settings()
|
|
mode = str(db.get_setting("exit_mode", s.exit_mode) or s.exit_mode)
|
|
if mode not in ("fixed_usdt", "premium_multiple"):
|
|
mode = "fixed_usdt"
|
|
return {
|
|
"fee_rate": float(db.get_setting("fee_rate", str(s.fee_rate)) or s.fee_rate),
|
|
"exit_move_pct": float(
|
|
db.get_setting("exit_move_pct", str(s.exit_move_pct)) or s.exit_move_pct
|
|
),
|
|
"exit_mode": mode,
|
|
"net_profit_target": float(
|
|
db.get_setting("net_profit_target", str(s.net_profit_target))
|
|
or s.net_profit_target
|
|
),
|
|
"premium_exit_multiple": float(
|
|
db.get_setting("premium_exit_multiple", str(s.premium_exit_multiple))
|
|
or s.premium_exit_multiple
|
|
),
|
|
"rest_seconds": int(
|
|
float(db.get_setting("rest_seconds", str(s.rest_seconds)) or s.rest_seconds)
|
|
),
|
|
"skip_weekends": _as_bool(
|
|
db.get_setting("skip_weekends", str(s.skip_weekends)), s.skip_weekends
|
|
),
|
|
"initial_equity": float(
|
|
db.get_setting("initial_equity", str(s.initial_equity)) or s.initial_equity
|
|
),
|
|
"leverage": float(db.get_setting("leverage", str(s.leverage)) or s.leverage),
|
|
"min_option_hours": float(
|
|
db.get_setting("min_option_hours", str(s.min_option_hours))
|
|
or s.min_option_hours
|
|
),
|
|
"min_option_leverage": float(
|
|
db.get_setting("min_option_leverage", str(s.min_option_leverage))
|
|
or s.min_option_leverage
|
|
),
|
|
"max_atm_open_offset": float(
|
|
db.get_setting("max_atm_open_offset", str(s.max_atm_open_offset))
|
|
or s.max_atm_open_offset
|
|
),
|
|
"close_bid_mark_max_pct": float(
|
|
db.get_setting("close_bid_mark_max_pct", str(s.close_bid_mark_max_pct))
|
|
or s.close_bid_mark_max_pct
|
|
),
|
|
"perp_qty_eth": float(
|
|
db.get_setting("perp_qty_eth", str(s.perp_qty_eth)) or s.perp_qty_eth
|
|
),
|
|
"option_qty_eth": float(
|
|
db.get_setting("option_qty_eth", str(s.option_qty_eth)) or s.option_qty_eth
|
|
),
|
|
"exchange": rt.exchange,
|
|
"perp_inst_id": rt.perp_inst_id,
|
|
"option_inst_family": rt.option_inst_family,
|
|
"index_inst_id": rt.index_inst_id,
|
|
"ledger": Ledger(db).snapshot(),
|
|
}
|
|
|
|
|
|
@router.get("/strategy")
|
|
async def get_strategy_settings(_user: Annotated[str, Depends(require_user)]) -> dict:
|
|
return _read_settings()
|
|
|
|
|
|
@router.put("/strategy")
|
|
async def put_strategy_settings(
|
|
body: StrategySettingsBody,
|
|
_user: Annotated[str, Depends(require_user)],
|
|
) -> dict:
|
|
db = get_db()
|
|
s = get_settings()
|
|
data = body.model_dump(exclude_none=True)
|
|
equity_to_apply: float | None = None
|
|
switch_to: str | None = None
|
|
|
|
if "exchange" in data:
|
|
new_ex = normalize_exchange_name(str(data.pop("exchange")))
|
|
old_ex = normalize_exchange_name(
|
|
db.get_setting("exchange", s.exchange) or s.exchange
|
|
)
|
|
if new_ex != old_ex:
|
|
if Matcher(db).has_open_position():
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="有未平仓,无法切换交易所;请先平仓后再改",
|
|
)
|
|
switch_to = new_ex
|
|
|
|
if "initial_equity" in data:
|
|
new_eq = float(data["initial_equity"])
|
|
old_eq = float(
|
|
db.get_setting("initial_equity", str(s.initial_equity)) or s.initial_equity
|
|
)
|
|
if abs(new_eq - old_eq) > 1e-9:
|
|
if Matcher(db).has_open_position():
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="有未平仓,无法重置模拟资金;请先平仓后再改",
|
|
)
|
|
equity_to_apply = new_eq
|
|
|
|
for k, v in data.items():
|
|
if k in KEYS:
|
|
db.set_setting(k, str(v))
|
|
|
|
if equity_to_apply is not None:
|
|
Ledger(db).reset_equity(
|
|
equity_to_apply,
|
|
note=f"设置模拟资金={equity_to_apply:.2f}",
|
|
)
|
|
|
|
if switch_to is not None:
|
|
rt = persist_exchange_choice(switch_to)
|
|
try:
|
|
await reload_market_session(rt)
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=502,
|
|
detail=f"交易所已切换为 {switch_to},但行情重连失败: {e}",
|
|
) from e
|
|
|
|
return _read_settings()
|