feat: paper trading signal flip - reverse signal closes existing position then opens new
This commit is contained in:
parent
f90df6f3b5
commit
66810701fb
@ -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:
|
||||
|
||||
@ -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}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-2 py-1.5 text-right font-mono">{t.score}</td>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user