diff --git a/backend/signal_engine.py b/backend/signal_engine.py index fd53b0d..da7aa0b 100644 --- a/backend/signal_engine.py +++ b/backend/signal_engine.py @@ -607,6 +607,43 @@ def paper_has_active_position(symbol: str) -> bool: return cur.fetchone()[0] > 0 +def paper_get_active_direction(symbol: str) -> str | None: + """获取该币种活跃持仓的方向,无持仓返回None""" + with get_sync_conn() as conn: + with conn.cursor() as cur: + cur.execute("SELECT direction FROM paper_trades WHERE symbol=%s AND status IN ('active','tp1_hit') LIMIT 1", (symbol,)) + row = cur.fetchone() + return row[0] if row else None + + +def paper_close_by_signal(symbol: str, current_price: float, now_ms: int): + """反向信号平仓:按当前价平掉该币种所有活跃仓位""" + with get_sync_conn() as conn: + with conn.cursor() as cur: + cur.execute( + "SELECT id, direction, entry_price, tp1_hit, atr_at_entry " + "FROM paper_trades WHERE symbol=%s AND status IN ('active','tp1_hit')", + (symbol,) + ) + positions = cur.fetchall() + for pos in positions: + pid, direction, entry_price, tp1_hit, atr_entry = pos + risk_distance = 2.0 * 0.7 * atr_entry if atr_entry > 0 else 1 + if direction == "LONG": + pnl_r = (current_price - entry_price) / risk_distance if risk_distance > 0 else 0 + else: + pnl_r = (entry_price - current_price) / risk_distance if risk_distance > 0 else 0 + # 扣手续费 + fee_r = (2 * PAPER_FEE_RATE * entry_price) / risk_distance if risk_distance > 0 else 0 + pnl_r -= fee_r + cur.execute( + "UPDATE paper_trades SET status='signal_flip', exit_price=%s, exit_ts=%s, pnl_r=%s WHERE id=%s", + (current_price, now_ms, round(pnl_r, 4), pid) + ) + logger.info(f"[{symbol}] 📝 反向信号平仓: {direction} @ {current_price:.2f} pnl={pnl_r:+.2f}R") + conn.commit() + + def paper_active_count() -> int: """当前所有币种活跃持仓总数""" with get_sync_conn() as conn: @@ -651,15 +688,24 @@ def main(): if result.get("signal"): logger.info(f"[{sym}] 🚨 信号: {result['signal']} score={result['score']} price={result['price']:.1f}") # 模拟盘开仓(需开关开启) - 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 PAPER_TRADING_ENABLED: + existing_dir = paper_get_active_direction(sym) + new_dir = result["signal"] + + if existing_dir and existing_dir != new_dir: + # 反向信号:先平掉现有仓位 + paper_close_by_signal(sym, result["price"], now_ms) + logger.info(f"[{sym}] 📝 反向信号平仓: {existing_dir} → {new_dir}") + + if 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 PAPER_TRADING_ENABLED and result.get("price") and result["price"] > 0: diff --git a/frontend/app/paper/page.tsx b/frontend/app/paper/page.tsx index 1bee316..48fce07 100644 --- a/frontend/app/paper/page.tsx +++ b/frontend/app/paper/page.tsx @@ -268,9 +268,10 @@ function TradeHistory() { t.status === "tp" ? "bg-emerald-100 text-emerald-700" : t.status === "sl" ? "bg-red-100 text-red-700" : t.status === "sl_be" ? "bg-amber-100 text-amber-700" : + t.status === "signal_flip" ? "bg-purple-100 text-purple-700" : "bg-slate-100 text-slate-600" }`}> - {t.status === "tp" ? "止盈" : t.status === "sl" ? "止损" : t.status === "sl_be" ? "保本" : t.status === "timeout" ? "超时" : t.status} + {t.status === "tp" ? "止盈" : t.status === "sl" ? "止损" : t.status === "sl_be" ? "保本" : t.status === "timeout" ? "超时" : t.status === "signal_flip" ? "翻转" : t.status} {t.score}