Fix Gate/Binance memory regression and roll stop offset from avg.

Stop fetch_tickers fallback for volume rank and keep stale cache on failed refresh. Compute roll unified stop as merge-average plus offset percent instead of break-even.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-24 00:21:07 +08:00
parent 7f8ae97a98
commit f63f8810e6
9 changed files with 343 additions and 37 deletions
+154 -14
View File
@@ -7,6 +7,7 @@ from fib_key_monitor_lib import calc_fib_plan, fib_ratio_from_type
ROLL_MAX_LEGS_LONG = 3
ROLL_MAX_LEGS_SHORT = 3
ROLL_STOP_OFFSET_PCT_DEFAULT = 1.0
FIB_MODES = frozenset({"fib_618", "fib_786"})
@@ -42,6 +43,105 @@ def max_roll_legs(direction: str) -> int:
return ROLL_MAX_LEGS_LONG if (direction or "long").strip().lower() == "long" else ROLL_MAX_LEGS_SHORT
def resolve_roll_stop_spec(
*,
new_stop_loss: Optional[float] = None,
stop_offset_pct: Optional[float] = None,
entry_ref: float = 0.0,
) -> tuple[str, float]:
"""
解析滚仓止损输入。
- stop_offset_pct:相对合并均价的偏移%,如 1 表示 1%(多:均价下方;空:均价上方)。
- new_stop_loss:兼容旧版绝对止损价;若数值很小(如 1.0)且相对均价过低,视为偏移%
"""
if stop_offset_pct is not None:
try:
pct = float(stop_offset_pct)
if pct > 0:
return "offset", pct
except (TypeError, ValueError):
pass
if new_stop_loss is not None:
try:
sl = float(new_stop_loss)
if sl > 0:
ref = float(entry_ref or 0)
if ref > 0 and sl <= min(30.0, ref * 0.25):
return "offset", sl
return "absolute", sl
except (TypeError, ValueError):
pass
return "offset", ROLL_STOP_OFFSET_PCT_DEFAULT
def unified_stop_from_avg(direction: str, avg: float, offset_pct: float) -> float:
"""合并均价 ± offset% 作为新统一止损(非保本)。"""
avg_f = float(avg)
pct = float(offset_pct) / 100.0
if avg_f <= 0 or pct <= 0:
return 0.0
direction = (direction or "long").strip().lower()
if direction == "short":
return avg_f * (1.0 + pct)
return avg_f * (1.0 - pct)
def avg_entry_after_add(
qty_existing: float,
entry_existing: float,
add_qty: float,
add_price: float,
) -> float:
q1 = float(qty_existing)
e1 = float(entry_existing)
q2 = float(add_qty)
e2 = float(add_price)
total = q1 + q2
if total <= 0:
return 0.0
return (q1 * e1 + q2 * e2) / total
def solve_add_amount_for_avg_stop_offset(
direction: str,
qty_existing: float,
entry_existing: float,
add_price: float,
offset_pct: float,
risk_budget_usdt: float,
) -> Tuple[Optional[float], Optional[str]]:
"""
合并后止损 = 合并均价 ± offset%,且触及止损时总亏损 ≈ risk_budget。
loss = offset% × (Q1·E1 + Q2·E2) => Q2 = (B/p Q1·E1) / E2
"""
try:
q1 = float(qty_existing)
e1 = float(entry_existing)
e2 = float(add_price)
b = float(risk_budget_usdt)
p = float(offset_pct) / 100.0
except (TypeError, ValueError):
return None, "参数格式错误"
if q1 <= 0 or e1 <= 0 or e2 <= 0 or b <= 0:
return None, "持仓或风险预算无效"
if p <= 0 or p >= 1:
return None, "止损偏移%须大于 0 且小于 100"
direction = (direction or "long").strip().lower()
need_notional = b / p
q2 = (need_notional - q1 * e1) / e2
if q2 <= 0:
return None, "按当前偏移%与总风险%,无需加仓或无法再加(已满足风险上限)"
new_avg = avg_entry_after_add(q1, e1, q2, e2)
sl = unified_stop_from_avg(direction, new_avg, offset_pct)
if direction == "short":
if sl <= e2:
return None, "做空:合并后止损须高于加仓价(请减小偏移%或风险%"
else:
if sl >= e2:
return None, "做多:合并后止损须低于加仓价(请减小偏移%或风险%"
return q2, None
def solve_add_amount_for_total_risk(
direction: str,
qty_existing: float,
@@ -90,7 +190,8 @@ def preview_roll(
entry_existing: float,
initial_take_profit: float,
add_mode: str,
new_stop_loss: float,
new_stop_loss: Optional[float] = None,
stop_offset_pct: Optional[float] = None,
risk_percent: float,
capital_base_usdt: float,
add_price: Optional[float] = None,
@@ -117,31 +218,49 @@ def preview_roll(
else:
return None, "加仓方式无效"
try:
sl = float(new_stop_loss)
tp = float(initial_take_profit)
except (TypeError, ValueError):
return None, "损/止盈格式错误"
if sl <= 0 or tp <= 0:
return None, "止损与首仓止盈须大于0"
return None, "止盈格式错误"
if tp <= 0:
return None, "首仓止盈须大于0"
stop_mode, stop_val = resolve_roll_stop_spec(
new_stop_loss=new_stop_loss,
stop_offset_pct=stop_offset_pct,
entry_ref=entry_existing,
)
if direction == "long":
if sl >= entry_add:
return None, "做多:新止损须低于加仓价"
if tp <= entry_existing:
return None, "做多:首仓止盈须高于当前持仓均价参考"
else:
if sl <= entry_add:
return None, "做空:新止损须高于加仓价"
if tp >= entry_existing:
return None, "做空:首仓止盈须低于当前持仓均价参考"
risk_budget = float(capital_base_usdt) * (float(risk_percent) / 100.0)
q2_raw, err = solve_add_amount_for_total_risk(
direction, qty_existing, entry_existing, entry_add, sl, risk_budget
)
offset_pct: Optional[float] = None
if stop_mode == "offset":
offset_pct = float(stop_val)
q2_raw, err = solve_add_amount_for_avg_stop_offset(
direction, qty_existing, entry_existing, entry_add, offset_pct, risk_budget
)
else:
sl = float(stop_val)
if sl <= 0:
return None, "止损须大于0"
if direction == "long":
if sl >= entry_add:
return None, "做多:新止损须低于加仓价"
else:
if sl <= entry_add:
return None, "做空:新止损须高于加仓价"
q2_raw, err = solve_add_amount_for_total_risk(
direction, qty_existing, entry_existing, entry_add, sl, risk_budget
)
if err:
return None, err
q2 = float(q2_raw)
new_qty = qty_existing + q2
new_avg = (qty_existing * entry_existing + q2 * entry_add) / new_qty
new_avg = avg_entry_after_add(qty_existing, entry_existing, q2, entry_add)
if stop_mode == "offset":
sl = unified_stop_from_avg(direction, new_avg, offset_pct)
if direction == "long":
loss_at_sl = (new_avg - sl) * new_qty
reward_at_tp = (tp - new_avg) * new_qty
@@ -154,11 +273,15 @@ def preview_roll(
"add_mode": mode,
"add_mode_label": mode_label,
"add_price": round(entry_add, 10),
"new_stop_loss": sl,
"new_stop_loss": round(sl, 10),
"stop_offset_pct": offset_pct,
"stop_mode": stop_mode,
"initial_take_profit": tp,
"risk_percent": float(risk_percent),
"risk_budget_usdt": round(risk_budget, 4),
"add_amount_raw": q2,
"qty_existing": float(qty_existing),
"entry_existing": float(entry_existing),
"qty_after": new_qty,
"avg_entry_after": round(new_avg, 10),
"loss_at_sl_usdt": round(loss_at_sl, 4),
@@ -168,3 +291,20 @@ def preview_roll(
"fib_upper": fib_upper,
"fib_lower": fib_lower,
}, None
def roll_stop_after_fill(
direction: str,
qty_before: float,
entry_before: float,
add_qty: float,
fill_price: float,
*,
stop_offset_pct: Optional[float] = None,
absolute_stop: Optional[float] = None,
) -> float:
"""成交后按合并均价重算统一止损(偏移%模式)或沿用绝对止损。"""
if stop_offset_pct is not None and float(stop_offset_pct) > 0:
avg = avg_entry_after_add(qty_before, entry_before, add_qty, fill_price)
return unified_stop_from_avg(direction, avg, float(stop_offset_pct))
return float(absolute_stop or 0)