fix: remove 0.7 ATR multiplier, store risk_distance in DB

- Removed 0.7*ATR multiplier from signal_engine and paper_monitor
- risk_distance now stored per-trade in paper_trades table
- paper_monitor/signal_engine read risk_distance from DB (no hardcoded values)
- V5.1 TP/SL: 2.0/1.5/3.0 -> 1.4/1.05/2.1 (same actual distance)
- V5.2 TP/SL: 3.0/2.0/4.5 -> 2.1/1.4/3.15 (same actual distance)
- Fixed V5.2 pnl_r: all historical values corrected by *2/3
- API unrealized_pnl_r also reads from DB risk_distance
This commit is contained in:
root 2026-03-02 07:00:05 +00:00
parent 02a769f513
commit 72ea0ffd0e
5 changed files with 32 additions and 34 deletions

View File

@ -753,13 +753,12 @@ async def paper_positions(
d["current_price"] = current_price d["current_price"] = current_price
# 浮动盈亏(R) # 浮动盈亏(R)
entry = r["entry_price"] entry = r["entry_price"]
atr = r["atr_at_entry"] or 1 rd = r.get("risk_distance") or abs(entry - r["sl_price"]) or 1
risk_distance = 2.0 * 0.7 * atr if rd > 0 and entry > 0:
if risk_distance > 0 and entry > 0:
if r["direction"] == "LONG": if r["direction"] == "LONG":
d["unrealized_pnl_r"] = round((current_price - entry) / risk_distance, 2) d["unrealized_pnl_r"] = round((current_price - entry) / rd, 2)
else: else:
d["unrealized_pnl_r"] = round((entry - current_price) / risk_distance, 2) d["unrealized_pnl_r"] = round((entry - current_price) / rd, 2)
# 浮动盈亏(USDT) — 假设1R = risk_per_trade # 浮动盈亏(USDT) — 假设1R = risk_per_trade
d["unrealized_pnl_usdt"] = round(d["unrealized_pnl_r"] * 200, 2) # 2% of 10000 d["unrealized_pnl_usdt"] = round(d["unrealized_pnl_r"] * 200, 2) # 2% of 10000
else: else:

View File

@ -49,20 +49,20 @@ def check_and_close(symbol_upper: str, price: float):
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute( cur.execute(
"SELECT id, direction, entry_price, tp1_price, tp2_price, sl_price, " "SELECT id, direction, entry_price, tp1_price, tp2_price, sl_price, "
"tp1_hit, entry_ts, atr_at_entry " "tp1_hit, entry_ts, atr_at_entry, risk_distance "
"FROM paper_trades WHERE symbol=%s AND status IN ('active','tp1_hit')", "FROM paper_trades WHERE symbol=%s AND status IN ('active','tp1_hit')",
(symbol_upper,) (symbol_upper,)
) )
positions = cur.fetchall() positions = cur.fetchall()
for pos in positions: for pos in positions:
pid, direction, entry_price, tp1, tp2, sl, tp1_hit, entry_ts, atr_entry = pos pid, direction, entry_price, tp1, tp2, sl, tp1_hit, entry_ts, atr_entry, rd_db = pos
closed = False closed = False
new_status = None new_status = None
pnl_r = 0.0 pnl_r = 0.0
# 统一计算risk_distance (1R基准距离) # 从DB读risk_distancefallback用|entry-sl|
risk_distance = 2.0 * 0.7 * atr_entry if atr_entry > 0 else 1 risk_distance = rd_db if rd_db and rd_db > 0 else abs(entry_price - sl)
# === 实盘模拟TP/SL视为限价单以挂单价成交非市价 === # === 实盘模拟TP/SL视为限价单以挂单价成交非市价 ===
if direction == "LONG": if direction == "LONG":
@ -128,7 +128,6 @@ def check_and_close(symbol_upper: str, price: float):
if closed: if closed:
# 扣手续费 # 扣手续费
risk_distance = 2.0 * 0.7 * atr_entry if atr_entry > 0 else 1
fee_r = (2 * FEE_RATE * entry_price) / risk_distance if risk_distance > 0 else 0 fee_r = (2 * FEE_RATE * entry_price) / risk_distance if risk_distance > 0 else 0
pnl_r -= fee_r pnl_r -= fee_r

View File

@ -736,26 +736,26 @@ def paper_open_trade(
): ):
"""模拟开仓""" """模拟开仓"""
import json as _json3 import json as _json3
risk_atr = 0.7 * atr if atr <= 0:
if risk_atr <= 0:
return return
sl_multiplier = float((tp_sl or {}).get("sl_multiplier", 2.0)) sl_multiplier = float((tp_sl or {}).get("sl_multiplier", 2.0))
tp1_multiplier = float((tp_sl or {}).get("tp1_multiplier", 1.5)) tp1_multiplier = float((tp_sl or {}).get("tp1_multiplier", 1.5))
tp2_multiplier = float((tp_sl or {}).get("tp2_multiplier", 3.0)) tp2_multiplier = float((tp_sl or {}).get("tp2_multiplier", 3.0))
risk_distance = sl_multiplier * atr # 1R = SL距离 = sl_multiplier × ATR
if direction == "LONG": if direction == "LONG":
sl = price - sl_multiplier * risk_atr sl = price - sl_multiplier * atr
tp1 = price + tp1_multiplier * risk_atr tp1 = price + tp1_multiplier * atr
tp2 = price + tp2_multiplier * risk_atr tp2 = price + tp2_multiplier * atr
else: else:
sl = price + sl_multiplier * risk_atr sl = price + sl_multiplier * atr
tp1 = price - tp1_multiplier * risk_atr tp1 = price - tp1_multiplier * atr
tp2 = price - tp2_multiplier * risk_atr tp2 = price - tp2_multiplier * atr
with get_sync_conn() as conn: with get_sync_conn() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute( cur.execute(
"INSERT INTO paper_trades (symbol,direction,score,tier,entry_price,entry_ts,tp1_price,tp2_price,sl_price,atr_at_entry,score_factors,strategy) " "INSERT INTO paper_trades (symbol,direction,score,tier,entry_price,entry_ts,tp1_price,tp2_price,sl_price,atr_at_entry,score_factors,strategy,risk_distance) "
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)",
( (
symbol, symbol,
direction, direction,
@ -769,6 +769,7 @@ def paper_open_trade(
atr, atr,
_json3.dumps(factors) if factors else None, _json3.dumps(factors) if factors else None,
strategy, strategy,
risk_distance,
), ),
) )
conn.commit() conn.commit()
@ -783,18 +784,18 @@ def paper_check_positions(symbol: str, current_price: float, now_ms: int):
with get_sync_conn() as conn: with get_sync_conn() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute( cur.execute(
"SELECT id, direction, entry_price, tp1_price, tp2_price, sl_price, tp1_hit, entry_ts, atr_at_entry " "SELECT id, direction, entry_price, tp1_price, tp2_price, sl_price, tp1_hit, entry_ts, atr_at_entry, risk_distance "
"FROM paper_trades WHERE symbol=%s AND status IN ('active','tp1_hit') ORDER BY id", "FROM paper_trades WHERE symbol=%s AND status IN ('active','tp1_hit') ORDER BY id",
(symbol,) (symbol,)
) )
positions = cur.fetchall() positions = cur.fetchall()
for pos in positions: for pos in positions:
pid, direction, entry_price, tp1, tp2, sl, tp1_hit, entry_ts, atr_entry = pos pid, direction, entry_price, tp1, tp2, sl, tp1_hit, entry_ts, atr_entry, rd_db = pos
closed = False closed = False
new_status = None new_status = None
pnl_r = 0.0 pnl_r = 0.0
risk_distance = 2.0 * 0.7 * atr_entry if atr_entry > 0 else 1 risk_distance = rd_db if rd_db and rd_db > 0 else abs(entry_price - sl)
# === 实盘模拟TP/SL视为限价单以挂单价成交 === # === 实盘模拟TP/SL视为限价单以挂单价成交 ===
if direction == "LONG": if direction == "LONG":
@ -859,7 +860,6 @@ def paper_check_positions(symbol: str, current_price: float, now_ms: int):
if closed: if closed:
# 扣除手续费(开仓+平仓各Taker 0.05% # 扣除手续费(开仓+平仓各Taker 0.05%
risk_distance = 2.0 * 0.7 * atr_entry if atr_entry > 0 else 1
fee_r = (2 * PAPER_FEE_RATE * entry_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 pnl_r -= fee_r
@ -910,20 +910,20 @@ def paper_close_by_signal(symbol: str, current_price: float, now_ms: int, strate
with conn.cursor() as cur: with conn.cursor() as cur:
if strategy: if strategy:
cur.execute( cur.execute(
"SELECT id, direction, entry_price, tp1_hit, atr_at_entry " "SELECT id, direction, entry_price, tp1_hit, atr_at_entry, risk_distance "
"FROM paper_trades WHERE symbol=%s AND strategy=%s AND status IN ('active','tp1_hit')", "FROM paper_trades WHERE symbol=%s AND strategy=%s AND status IN ('active','tp1_hit')",
(symbol, strategy), (symbol, strategy),
) )
else: else:
cur.execute( cur.execute(
"SELECT id, direction, entry_price, tp1_hit, atr_at_entry " "SELECT id, direction, entry_price, tp1_hit, atr_at_entry, risk_distance "
"FROM paper_trades WHERE symbol=%s AND status IN ('active','tp1_hit')", "FROM paper_trades WHERE symbol=%s AND status IN ('active','tp1_hit')",
(symbol,), (symbol,),
) )
positions = cur.fetchall() positions = cur.fetchall()
for pos in positions: for pos in positions:
pid, direction, entry_price, tp1_hit, atr_entry = pos pid, direction, entry_price, tp1_hit, atr_entry, rd_db = pos
risk_distance = 2.0 * 0.7 * atr_entry if atr_entry > 0 else 1 risk_distance = rd_db if rd_db and rd_db > 0 else abs(entry_price * 0.01)
if direction == "LONG": if direction == "LONG":
pnl_r = (current_price - entry_price) / risk_distance if risk_distance > 0 else 0 pnl_r = (current_price - entry_price) / risk_distance if risk_distance > 0 else 0
else: else:

View File

@ -11,9 +11,9 @@
}, },
"accel_bonus": 5, "accel_bonus": 5,
"tp_sl": { "tp_sl": {
"sl_multiplier": 2.0, "sl_multiplier": 1.4,
"tp1_multiplier": 1.5, "tp1_multiplier": 1.05,
"tp2_multiplier": 3.0 "tp2_multiplier": 2.1
}, },
"signals": ["cvd", "p99", "accel", "ls_ratio", "oi", "coinbase_premium"] "signals": ["cvd", "p99", "accel", "ls_ratio", "oi", "coinbase_premium"]
} }

View File

@ -13,9 +13,9 @@
}, },
"accel_bonus": 0, "accel_bonus": 0,
"tp_sl": { "tp_sl": {
"sl_multiplier": 3.0, "sl_multiplier": 2.1,
"tp1_multiplier": 2.0, "tp1_multiplier": 1.4,
"tp2_multiplier": 4.5 "tp2_multiplier": 3.15
}, },
"signals": ["cvd", "p99", "accel", "ls_ratio", "oi", "coinbase_premium", "funding_rate", "liquidation"] "signals": ["cvd", "p99", "accel", "ls_ratio", "oi", "coinbase_premium", "funding_rate", "liquidation"]
} }