From 282aed138a51a20da3c97debf35d33989e2820a5 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 28 Feb 2026 11:13:39 +0000 Subject: [PATCH] feat: paper trading switch + config API + max positions limit --- backend/main.py | 33 +++++++++++++++++++++ backend/signal_engine.py | 58 +++++++++++++++++++++++++++++++------ frontend/app/paper/page.tsx | 50 ++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 9 deletions(-) diff --git a/backend/main.py b/backend/main.py index f264246..5b6c772 100644 --- a/backend/main.py +++ b/backend/main.py @@ -500,6 +500,39 @@ async def get_signal_trades( # ─── 模拟盘 API ────────────────────────────────────────────────── +# 模拟盘配置状态(与signal_engine共享的运行时状态) +paper_config = { + "enabled": False, + "initial_balance": 10000, + "risk_per_trade": 0.02, + "max_positions": 4, + "tier_multiplier": {"light": 0.5, "standard": 1.0, "heavy": 1.5}, +} + + +@app.get("/api/paper/config") +async def paper_get_config(user: dict = Depends(get_current_user)): + """获取模拟盘配置""" + return paper_config + + +@app.post("/api/paper/config") +async def paper_set_config(request: Request, user: dict = Depends(get_current_user)): + """修改模拟盘配置(仅admin)""" + if user.get("role") != "admin": + raise HTTPException(status_code=403, detail="仅管理员可修改") + body = await request.json() + for k in ["enabled", "initial_balance", "risk_per_trade", "max_positions"]: + if k in body: + paper_config[k] = body[k] + # 写入配置文件让signal_engine也能读到 + import json + config_path = os.path.join(os.path.dirname(__file__), "paper_config.json") + with open(config_path, "w") as f: + json.dump(paper_config, f, indent=2) + return {"ok": True, "config": paper_config} + + @app.get("/api/paper/summary") async def paper_summary(user: dict = Depends(get_current_user)): """模拟盘总览""" diff --git a/backend/signal_engine.py b/backend/signal_engine.py index b017564..d919479 100644 --- a/backend/signal_engine.py +++ b/backend/signal_engine.py @@ -37,6 +37,33 @@ logger = logging.getLogger("signal-engine") SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"] LOOP_INTERVAL = 15 # 秒(从5改15,CPU降60%,信号质量无影响) +# ─── 模拟盘配置 ─────────────────────────────────────────────────── +PAPER_TRADING_ENABLED = False # 开关(范总确认后通过API开启) +PAPER_INITIAL_BALANCE = 10000 # 虚拟初始资金 USDT +PAPER_RISK_PER_TRADE = 0.02 # 单笔风险 2%(即200U) +PAPER_MAX_POSITIONS = 4 # 最大同时持仓数 +PAPER_TIER_MULTIPLIER = { # 档位仓位倍数 + "light": 0.5, # 轻仓: 1% + "standard": 1.0, # 标准: 2% + "heavy": 1.5, # 加仓: 3% +} + +def load_paper_config(): + """从配置文件加载模拟盘开关和参数""" + global PAPER_TRADING_ENABLED, PAPER_INITIAL_BALANCE, PAPER_RISK_PER_TRADE, PAPER_MAX_POSITIONS + config_path = os.path.join(os.path.dirname(__file__), "paper_config.json") + try: + with open(config_path, "r") as f: + import json as _json2 + cfg = _json2.load(f) + PAPER_TRADING_ENABLED = cfg.get("enabled", False) + PAPER_INITIAL_BALANCE = cfg.get("initial_balance", 10000) + PAPER_RISK_PER_TRADE = cfg.get("risk_per_trade", 0.02) + PAPER_MAX_POSITIONS = cfg.get("max_positions", 4) + except FileNotFoundError: + pass +# ───────────────────────────────────────────────────────────────── + # 窗口大小(毫秒) WINDOW_FAST = 30 * 60 * 1000 # 30分钟 WINDOW_MID = 4 * 3600 * 1000 # 4小时 @@ -574,6 +601,14 @@ def paper_has_active_position(symbol: str) -> bool: return cur.fetchone()[0] > 0 +def paper_active_count() -> int: + """当前所有币种活跃持仓总数""" + with get_sync_conn() as conn: + with conn.cursor() as cur: + cur.execute("SELECT COUNT(*) FROM paper_trades WHERE status IN ('active','tp1_hit')") + return cur.fetchone()[0] + + # ─── 主循环 ────────────────────────────────────────────────────── def main(): @@ -591,6 +626,8 @@ def main(): while True: try: now_ms = int(time.time() * 1000) + # 每轮重新加载配置(支持API热更新开关) + load_paper_config() for sym, state in states.items(): new_trades = fetch_new_trades(sym, state.last_processed_id) for t in new_trades: @@ -607,16 +644,19 @@ def main(): if result.get("signal"): logger.info(f"[{sym}] 🚨 信号: {result['signal']} score={result['score']} price={result['price']:.1f}") - # 模拟盘开仓 - if not paper_has_active_position(sym): - paper_open_trade( - sym, result["signal"], result["price"], - result["score"], result.get("tier", "standard"), - result["atr"], now_ms - ) + # 模拟盘开仓(需开关开启) + if PAPER_TRADING_ENABLED and not paper_has_active_position(sym): + active_count = paper_active_count() + if active_count < PAPER_MAX_POSITIONS: + tier = result.get("tier", "standard") + paper_open_trade( + sym, result["signal"], result["price"], + result["score"], tier, + result["atr"], now_ms + ) - # 模拟盘持仓检查(每次循环都检查,不管有没有新信号) - if result.get("price") and result["price"] > 0: + # 模拟盘持仓检查(开关开着才检查) + if PAPER_TRADING_ENABLED and result.get("price") and result["price"] > 0: paper_check_positions(sym, result["price"], now_ms) cycle += 1 diff --git a/frontend/app/paper/page.tsx b/frontend/app/paper/page.tsx index 5f923a7..a42ca33 100644 --- a/frontend/app/paper/page.tsx +++ b/frontend/app/paper/page.tsx @@ -15,6 +15,55 @@ function fmtPrice(p: number) { return p < 100 ? p.toFixed(4) : p.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 }); } +// ─── 控制面板(开关+配置)────────────────────────────────────── + +function ControlPanel() { + const [config, setConfig] = useState(null); + const [saving, setSaving] = useState(false); + + useEffect(() => { + const f = async () => { try { const r = await authFetch("/api/paper/config"); if (r.ok) setConfig(await r.json()); } catch {} }; + f(); + }, []); + + const toggle = async () => { + setSaving(true); + try { + const r = await authFetch("/api/paper/config", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled: !config.enabled }), + }); + if (r.ok) setConfig(await r.json().then(j => j.config)); + } catch {} finally { setSaving(false); } + }; + + if (!config) return null; + + return ( +
+
+ + + {config.enabled ? "🟢 运行中" : "⚪ 已停止"} + +
+
+ 初始资金: ${config.initial_balance?.toLocaleString()} + 单笔风险: {(config.risk_per_trade * 100).toFixed(0)}% + 最大持仓: {config.max_positions} +
+
+ ); +} + // ─── 总览面板 ──────────────────────────────────────────────────── function SummaryCards() { @@ -288,6 +337,7 @@ export default function PaperTradingPage() {

V5.1信号引擎自动交易 · 实时追踪 · 数据驱动优化

+