feat: paper trading signal flip - reverse signal closes existing position then opens new

This commit is contained in:
root 2026-02-28 11:45:48 +00:00
parent f90df6f3b5
commit 66810701fb
2 changed files with 57 additions and 10 deletions

View File

@ -607,6 +607,43 @@ def paper_has_active_position(symbol: str) -> bool:
return cur.fetchone()[0] > 0 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: def paper_active_count() -> int:
"""当前所有币种活跃持仓总数""" """当前所有币种活跃持仓总数"""
with get_sync_conn() as conn: with get_sync_conn() as conn:
@ -651,15 +688,24 @@ def main():
if result.get("signal"): if result.get("signal"):
logger.info(f"[{sym}] 🚨 信号: {result['signal']} score={result['score']} price={result['price']:.1f}") logger.info(f"[{sym}] 🚨 信号: {result['signal']} score={result['score']} price={result['price']:.1f}")
# 模拟盘开仓(需开关开启) # 模拟盘开仓(需开关开启)
if PAPER_TRADING_ENABLED and not paper_has_active_position(sym): if PAPER_TRADING_ENABLED:
active_count = paper_active_count() existing_dir = paper_get_active_direction(sym)
if active_count < PAPER_MAX_POSITIONS: new_dir = result["signal"]
tier = result.get("tier", "standard")
paper_open_trade( if existing_dir and existing_dir != new_dir:
sym, result["signal"], result["price"], # 反向信号:先平掉现有仓位
result["score"], tier, paper_close_by_signal(sym, result["price"], now_ms)
result["atr"], 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: if PAPER_TRADING_ENABLED and result.get("price") and result["price"] > 0:

View File

@ -268,9 +268,10 @@ function TradeHistory() {
t.status === "tp" ? "bg-emerald-100 text-emerald-700" : t.status === "tp" ? "bg-emerald-100 text-emerald-700" :
t.status === "sl" ? "bg-red-100 text-red-700" : t.status === "sl" ? "bg-red-100 text-red-700" :
t.status === "sl_be" ? "bg-amber-100 text-amber-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" "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}
</span> </span>
</td> </td>
<td className="px-2 py-1.5 text-right font-mono">{t.score}</td> <td className="px-2 py-1.5 text-right font-mono">{t.score}</td>